├── .firebaserc ├── .gitignore ├── firebase.json ├── package.json ├── workbox-config.js ├── main.go ├── CONTRIBUTING.md ├── README.md ├── pageNav.go ├── header.go ├── user.go ├── storyList.go ├── storyCard.go ├── story.go ├── commentCard.go ├── app.go └── LICENSE /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "go-hn-fcda3" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | **/*.DS_Store 4 | *_reactGen.go 5 | build/assets/scripts/analytics.js -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "go-hacker-news", 4 | "version": "0.0.0", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "gopherjs build --output build/script.min.js --minify && npm run sw", 8 | "sw": "workbox generateSW workbox-config.js" 9 | }, 10 | "devDependencies": { 11 | "workbox-cli": "^3.6.2" 12 | }, 13 | "dependencies": {} 14 | } 15 | -------------------------------------------------------------------------------- /workbox-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | module.exports = { 18 | "globDirectory": "build/", 19 | "globPatterns": [ 20 | "**/*.{svg,html,js}" 21 | ], 22 | "swDest": "build/service-worker.js", 23 | "maximumFileSizeToCacheInBytes": 3 * 1024 * 1024 24 | }; -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "honnef.co/go/js/dom" 21 | "myitcv.io/react" 22 | ) 23 | 24 | //go:generate reactGen 25 | 26 | var document = dom.GetWindow().Document() 27 | 28 | func main() { 29 | domTarget := document.GetElementByID("app") 30 | 31 | react.Render(App(), domTarget) 32 | } 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are ALWAYS welcome. If you would like to modify, add or fix anything with the site itself (including adding/removing packages), please put up an issue first so we can chat about it! 4 | 5 | ## Contributor License Agreement 6 | 7 | Contributions to this project must be accompanied by a Contributor License 8 | Agreement. You (or your employer) retain the copyright to your contribution; 9 | this simply gives us permission to use and redistribute your contributions as 10 | part of the project. Head over to to see 11 | your current agreements on file or to sign a new one. 12 | 13 | You generally only need to submit a CLA once, so if you've already submitted one 14 | (even if it was for a different project), you probably don't need to do it 15 | again. 16 | 17 | ## Where can I contribute? 18 | 19 | **The major priority is minimizing the final bundle size and improving the general performance of the site.** Any suggestions here will be much appreciated! 20 | 21 | ### Improvements to consider 22 | 23 | 1. Reimplement fetch requests instead of XHR. The default `net/http` package is _extremely_ large unfortunately. [`fasthttp`](https://github.com/valyala/fasthttp) might be worth a shot. 24 | 2. Find a lighter alternative to `encoding/json` for JSON decoding. [`jsonparser`](https://github.com/buger/jsonparser) may be a suitable option. 25 | 3. Hosting on a different platform and using [Brotli](https://github.com/google/brotli) 26 | 4. Find a way to split bundle to separate route-specific chunks. 27 | - Split chunks by package and then dynamic import where possible? [JSGO](https://github.com/dave/jsgo) might be useful here. 28 | 5. Cache dynamic results using our service worker for offline support (need to implement fetch first!) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Go Hacker News 4 | 5 |

6 | 7 |

8 | A Hacker News client written in Go 9 |

10 | 11 |

12 | PRs Welcome 13 |

14 | 15 | ## What is this? 16 | 17 | A Hacker News client (yes, another one) written in [Go](https://golang.org/) using [GopherJS](https://github.com/gopherjs/gopherjs). 18 | 19 | ## What is this built with? 20 | 21 | * [GopherJS](https://github.com/gopherjs/gopherjs) to compile Go to JavaScript 22 | * [myitcv.io/react](https://github.com/myitcv/x/tree/master/react) for React bindings 23 | * [JSX](https://godoc.org/myitcv.io/react/jsx) is supported, but this app does not have any :) 24 | * [Humble/Router](https://github.com/go-humble/router) for routing 25 | 26 | ### Additional 27 | 28 | * Service Worker added with [Workbox](https://developers.google.com/web/tools/workbox/) 29 | * Hosting on [Firebase](https://www.google.ca/search?q=firebase+hosting&oq=firebase+hosting&aqs=chrome.0.0j69i60l2j69i61j0l2.1327j0j4&sourceid=chrome&ie=UTF-8) 30 | 31 | ## Setup 32 | 33 | 1. Fork/clone the repo 34 | 2. Install packages: 35 | 36 | ```bash 37 | go get -u github.com/gopherjs/gopherjs 38 | go get -u myitcv.io/react myitcv.io/react/cmd/reactGen 39 | go get -u honnef.co/go/js/xhr github.com/go-humble/router 40 | ``` 41 | 42 | 3. Add GopherJS and ReactGen to PATH: 43 | 44 | ```bash 45 | export PATH="$(dirname $(go list -f '{{.Target}}' myitcv.io/react/cmd/reactGen)):$PATH" 46 | ``` 47 | 48 | 4. Create generated files for each component: 49 | 50 | ```bash 51 | go generate 52 | ``` 53 | 54 | 5. Build the application: 55 | 56 | ```bash 57 | gopherjs build --output build/script.min.js --minify 58 | ``` 59 | 60 | This will save create `script.min.js` in the `build/` folder. You can use any local testing server in `build/` to boot up the application (for example: `python -m SimpleHTTPServer` if you have Python installed). 61 | 62 | ## Can I contribute? 63 | 64 | Of course you can! Please take a look at the [contributing](./CONTRIBUTING.md) documentation for more info. 65 | 66 | ## License 67 | 68 | Apache 2.0 69 | 70 | This is not an official Google product. 71 | -------------------------------------------------------------------------------- /pageNav.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "strconv" 21 | 22 | "myitcv.io/react" 23 | ) 24 | 25 | // PageNavDef is the definition for the PageNav component 26 | type PageNavDef struct { 27 | react.ComponentDef 28 | } 29 | 30 | // PageNavProps is the prop types for the PageNav component 31 | type PageNavProps struct { 32 | CurrPage int 33 | StoryType string 34 | NumStories int 35 | } 36 | 37 | // PageNav creates instances of the PageNav component 38 | func PageNav(p PageNavProps) *PageNavElem { 39 | return buildPageNavElem(PageNavProps{CurrPage: p.CurrPage, StoryType: p.StoryType, NumStories: p.NumStories}) 40 | } 41 | 42 | // Render renders the PageNav component 43 | func (f PageNavDef) Render() react.Element { 44 | props := f.Props() 45 | 46 | var prevLink react.Element 47 | var nextLink react.Element 48 | 49 | if props.CurrPage == 1 { 50 | prevLink = react.Span( 51 | &react.SpanProps{ClassName: "disabled"}, 52 | react.S("< prev"), 53 | ) 54 | } else { 55 | prevLink = react.A( 56 | &react.AProps{Href: "/#/" + props.StoryType + "/" + strconv.Itoa(props.CurrPage-1)}, 57 | react.S("< prev"), 58 | ) 59 | } 60 | 61 | if props.NumStories == 30 { 62 | nextLink = react.A( 63 | &react.AProps{Href: "/#/" + props.StoryType + "/" + strconv.Itoa(props.CurrPage+1)}, 64 | react.S("more >"), 65 | ) 66 | } else { 67 | nextLink = react.Span( 68 | &react.SpanProps{ClassName: "disabled"}, 69 | react.S("more >"), 70 | ) 71 | } 72 | 73 | return react.Div(&react.DivProps{ClassName: "page-nav-container"}, 74 | react.Div(&react.DivProps{ClassName: "page-nav"}, 75 | prevLink, 76 | react.Span( 77 | nil, 78 | react.S(react.S(strconv.Itoa(props.CurrPage))), 79 | ), 80 | nextLink, 81 | ), 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /header.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "myitcv.io/react" 21 | ) 22 | 23 | // HeaderDef is the definition for the Header component 24 | type HeaderDef struct { 25 | react.ComponentDef 26 | } 27 | 28 | // HeaderProps is the prop types for the Header component 29 | type HeaderProps struct { 30 | path string 31 | } 32 | 33 | // Header creates instances of the Header component 34 | func Header(h HeaderProps) *HeaderElem { 35 | return buildHeaderElem(HeaderProps{path: h.path}) 36 | } 37 | 38 | // Render renders the Header component 39 | func (f HeaderDef) Render() react.Element { 40 | return react.Header(&react.HeaderProps{ClassName: "header"}, 41 | f.renderNav(), 42 | ) 43 | } 44 | 45 | func (f HeaderDef) genLink(name string, link string, storyType string) react.Element { 46 | props := f.Props() 47 | 48 | class := "" 49 | 50 | if storyType == props.path { 51 | class = "active" 52 | } 53 | 54 | return react.A( 55 | &react.AProps{Href: link, ClassName: class}, 56 | react.S(name), 57 | ) 58 | } 59 | 60 | func (f HeaderDef) renderNav() *react.NavElem { 61 | links := []react.Element{ 62 | react.A( 63 | &react.AProps{Href: "#/"}, 64 | react.Img(&react.ImgProps{Src: "assets/logo.png", ClassName: "logo", Alt: "Golang logo"}), 65 | ), 66 | f.genLink("Top", "#/news/1", "news"), 67 | f.genLink("New", "#/newest/1", "newest"), 68 | f.genLink("Show", "#/show/1", "show"), 69 | f.genLink("Ask", "#/ask/1", "ask"), 70 | f.genLink("Jobs", "#/jobs/1", "jobs"), 71 | } 72 | 73 | return react.Nav(nil, 74 | react.Div(&react.DivProps{ClassName: "inner"}, 75 | links..., 76 | ), 77 | react.A( 78 | &react.AProps{Href: "https://github.com/GoogleChromeLabs/go-hackernews", ClassName: "github"}, 79 | react.S("Built with GopherJS"), 80 | ), 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "strconv" 21 | 22 | "myitcv.io/react" 23 | ) 24 | 25 | // UserDef is the definition for the User component 26 | type UserDef struct { 27 | react.ComponentDef 28 | } 29 | 30 | // UserProps is the prop types for the User component 31 | type UserProps struct { 32 | About string 33 | CreatedTime int 34 | Created string 35 | ID string 36 | Karma int 37 | } 38 | 39 | // User creates instances of the User component 40 | func User(p UserProps) *UserElem { 41 | return buildUserElem(UserProps{ID: p.ID, CreatedTime: p.CreatedTime, Created: p.Created, Karma: p.Karma, About: p.About}) 42 | } 43 | 44 | // Render renders the User component 45 | func (f UserDef) Render() react.Element { 46 | props := f.Props() 47 | 48 | return react.Div(nil, 49 | react.Div(&react.DivProps{ClassName: "wrapper"}, 50 | react.Div(&react.DivProps{ClassName: "user-view view"}, 51 | react.H1( 52 | nil, 53 | react.S("User : "+props.ID), 54 | ), 55 | react.Ul( 56 | &react.UlProps{ClassName: "meta"}, 57 | react.Li( 58 | nil, 59 | react.Span( 60 | &react.SpanProps{ClassName: "label"}, 61 | react.S("Created:"), 62 | ), 63 | react.S(" "+props.Created), 64 | ), 65 | react.Li( 66 | nil, 67 | react.Span( 68 | &react.SpanProps{ClassName: "label"}, 69 | react.S("Karma:"), 70 | ), 71 | react.S(" "+strconv.Itoa(props.Karma)), 72 | ), 73 | react.Li( 74 | &react.LiProps{ClassName: "about", DangerouslySetInnerHTML: react.NewDangerousInnerHTML(props.About)}, 75 | ), 76 | ), 77 | react.P( 78 | &react.PProps{ClassName: "links"}, 79 | react.A( 80 | &react.AProps{Target: "_blank", Href: "https://news.ycombinator.com/submitted?id=" + props.ID}, 81 | react.S("submissions"), 82 | ), 83 | react.S(" | "), 84 | react.A( 85 | &react.AProps{Target: "_blank", Href: "https://news.ycombinator.com/threads?id=" + props.ID}, 86 | react.S("comments"), 87 | ), 88 | ), 89 | ), 90 | ), 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /storyList.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "myitcv.io/react" 21 | ) 22 | 23 | // StoryListDef is the definition for the StoryList component 24 | type StoryListDef struct { 25 | react.ComponentDef 26 | } 27 | 28 | // StoryListProps is the prop types for the StoryList component 29 | type StoryListProps struct { 30 | StoryItems []story 31 | } 32 | 33 | // StoryList creates instances of the StoryList component 34 | func StoryList(p StoryListProps) *StoryListElem { 35 | return buildStoryListElem(StoryListProps{StoryItems: p.StoryItems}) 36 | } 37 | 38 | // Equals is used to define component re-rendering 39 | func (c StoryListProps) Equals(v StoryListProps) bool { 40 | if len(v.StoryItems) != len(c.StoryItems) { 41 | return false 42 | } 43 | 44 | for i := range v.StoryItems { 45 | if v.StoryItems[i] != c.StoryItems[i] { 46 | return false 47 | } 48 | } 49 | 50 | return true 51 | } 52 | 53 | // Render renders the StoryList component 54 | func (f StoryListDef) Render() react.Element { 55 | props := f.Props() 56 | 57 | var storyItems []react.RendersLi 58 | var storyList []react.RendersLi 59 | 60 | if len(props.StoryItems) > 0 { 61 | for _, story := range props.StoryItems { 62 | storyItems = append(storyItems, StoryCard(StoryCardProps{ 63 | ID: story.ID, 64 | title: story.Title, 65 | points: story.Points, 66 | commentsCount: story.CommentsCount, 67 | domain: story.Domain, 68 | timeAgo: story.TimeAgo, 69 | user: story.User, 70 | URL: story.URL, 71 | storyType: story.Type, 72 | })) 73 | } 74 | 75 | storyList = storyItems 76 | } else { 77 | storyList = append(storyList, react.Li(&react.LiProps{ClassName: "story-card"}, 78 | react.Span( 79 | &react.SpanProps{ClassName: "title"}, 80 | react.S("No more story items!")), 81 | )) 82 | } 83 | 84 | return react.Div(nil, 85 | react.Div(&react.DivProps{ClassName: "wrapper"}, 86 | react.Div(&react.DivProps{ClassName: "story-list view"}, 87 | react.Ul( 88 | nil, 89 | storyList..., 90 | ), 91 | ), 92 | ), 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /storyCard.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "strconv" 21 | "strings" 22 | 23 | "myitcv.io/react" 24 | ) 25 | 26 | // StoryCardDef is the definition for the StoryCard component 27 | type StoryCardDef struct { 28 | react.ComponentDef 29 | } 30 | 31 | // StoryCardProps is the prop types for the StoryCard component 32 | type StoryCardProps struct { 33 | ID int 34 | title string 35 | points int 36 | commentsCount int 37 | domain string 38 | timeAgo string 39 | user string 40 | storyType string 41 | URL string 42 | } 43 | 44 | // StoryCard creates instances of the StoryCard component 45 | func StoryCard(s StoryCardProps) *StoryCardElem { 46 | return buildStoryCardElem(StoryCardProps{ID: s.ID, title: s.title, points: s.points, commentsCount: s.commentsCount, domain: s.domain, timeAgo: s.timeAgo, user: s.user, URL: s.URL, storyType: s.storyType}) 47 | } 48 | 49 | // Render renders the StoryCard component 50 | func (f StoryCardDef) Render() *react.LiElem { 51 | props := f.Props() 52 | 53 | var userSpan react.Element 54 | if props.user == "" { 55 | userSpan = nil 56 | } else { 57 | userSpan = react.Span( 58 | &react.SpanProps{ClassName: "by"}, 59 | react.S(" by "), 60 | react.A( 61 | &react.AProps{Href: "#/user/" + props.user}, 62 | react.S(" "+props.user), 63 | ), 64 | ) 65 | } 66 | 67 | var commentsSpan react.Element 68 | if props.storyType == "job" { 69 | commentsSpan = nil 70 | } else { 71 | commentsSpan = react.Span( 72 | &react.SpanProps{ClassName: "comments-link"}, 73 | react.S(" | "), 74 | react.A( 75 | &react.AProps{Href: "#/item/" + strconv.Itoa(props.ID)}, 76 | react.S(strconv.Itoa(props.commentsCount)+" comments"), 77 | ), 78 | ) 79 | } 80 | 81 | link := props.URL 82 | if strings.HasPrefix(props.URL, "item") { 83 | link = "#/item/" + strconv.Itoa(props.ID) 84 | } 85 | 86 | domainStr := "" 87 | if props.domain != "" { 88 | domainStr = " (" + props.domain + ")" 89 | } 90 | 91 | return react.Li(&react.LiProps{ClassName: "story-card"}, 92 | react.Span( 93 | &react.SpanProps{ClassName: "score"}, 94 | react.S(strconv.Itoa(props.points)), 95 | ), 96 | react.Span( 97 | &react.SpanProps{ClassName: "title"}, 98 | react.A( 99 | &react.AProps{Href: link}, 100 | react.S(props.title), 101 | ), 102 | react.Span( 103 | &react.SpanProps{ClassName: "host"}, 104 | react.S(domainStr), 105 | ), 106 | ), 107 | react.Br(nil, nil), 108 | react.Span( 109 | &react.SpanProps{ClassName: "meta"}, 110 | userSpan, 111 | react.Span( 112 | &react.SpanProps{ClassName: "time"}, 113 | react.S(" "+props.timeAgo+" "), 114 | ), 115 | commentsSpan, 116 | ), 117 | ) 118 | } 119 | 120 | // RendersLi is used to return the rendered StoryCard component as a list item element 121 | func (f StoryCardDef) RendersLi(*react.LiElem) {} 122 | -------------------------------------------------------------------------------- /story.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "strconv" 21 | 22 | "myitcv.io/react" 23 | ) 24 | 25 | // StoryDef is the definition for the Story component 26 | type StoryDef struct { 27 | react.ComponentDef 28 | } 29 | 30 | // StoryProps is the prop types for the Story component 31 | type StoryProps struct { 32 | ID int 33 | Title string 34 | Points int 35 | User string 36 | Time int 37 | TimeAgo string 38 | Type string 39 | Content string 40 | Comments []comment 41 | CommentsCount int 42 | URL string 43 | Domain string 44 | } 45 | 46 | // Story creates instances of the Story component 47 | func Story(p StoryProps) *StoryElem { 48 | return buildStoryElem(StoryProps{ID: p.ID, Title: p.Title, Points: p.Points, User: p.User, Time: p.Time, TimeAgo: p.TimeAgo, Type: p.Type, Content: p.Content, Comments: p.Comments, CommentsCount: p.CommentsCount, URL: p.URL, Domain: p.Domain}) 49 | } 50 | 51 | // Equals is used to define component re-rendering 52 | func (c StoryProps) Equals(v StoryProps) bool { 53 | if c.ID != v.ID { 54 | return false 55 | } 56 | 57 | return true 58 | } 59 | 60 | // Render renders the Story component 61 | func (f StoryDef) Render() react.Element { 62 | props := f.Props() 63 | 64 | var comments []react.RendersLi 65 | 66 | if len(props.Comments) > 0 { 67 | for _, comment := range props.Comments { 68 | comments = append(comments, CommentCard(CommentCardProps{ 69 | ID: comment.ID, 70 | User: comment.User, 71 | Time: comment.Time, 72 | TimeAgo: comment.TimeAgo, 73 | Type: comment.Type, 74 | Content: comment.Content, 75 | Comments: comment.Comments, 76 | CommentsCount: comment.CommentsCount, 77 | Level: comment.Level, 78 | URL: comment.URL, 79 | Dead: comment.Dead, 80 | })) 81 | } 82 | } 83 | 84 | domainStr := "" 85 | if props.Domain != "" { 86 | domainStr = "(" + props.Domain + ")" 87 | } 88 | 89 | return react.Div(nil, 90 | react.Div(&react.DivProps{ClassName: "wrapper"}, 91 | react.Div(&react.DivProps{ClassName: "view"}, 92 | react.Div(&react.DivProps{ClassName: "item-view-header"}, 93 | react.A( 94 | &react.AProps{Target: "_blank", Href: props.URL, ClassName: "github"}, 95 | react.H1( 96 | nil, 97 | react.S(props.Title), 98 | ), 99 | ), 100 | react.Span( 101 | &react.SpanProps{ClassName: "host"}, 102 | react.S(domainStr), 103 | ), 104 | react.P( 105 | &react.PProps{ClassName: "meta"}, 106 | react.S(strconv.Itoa(props.Points)+" points | by "), 107 | react.A( 108 | &react.AProps{Href: "#/user/" + props.User}, 109 | react.S(props.User), 110 | ), 111 | react.S(" "+props.TimeAgo), 112 | ), 113 | ), 114 | react.Div(&react.DivProps{ClassName: "item-view-comments"}, 115 | react.P(&react.PProps{ClassName: "item-view-comments-header"}, 116 | react.S(strconv.Itoa(props.CommentsCount)+" comments"), 117 | ), 118 | react.Ul( 119 | &react.UlProps{ClassName: "comment-children"}, 120 | comments..., 121 | ), 122 | ), 123 | ), 124 | ), 125 | ) 126 | } 127 | -------------------------------------------------------------------------------- /commentCard.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "strconv" 21 | 22 | "myitcv.io/react" 23 | ) 24 | 25 | // CommentCardDef is the definition for the CommentCard component 26 | type CommentCardDef struct { 27 | react.ComponentDef 28 | } 29 | 30 | // CommentCardProps is the prop types for the CommentCard component 31 | type CommentCardProps struct { 32 | ID int 33 | User string 34 | Time int 35 | TimeAgo string 36 | Type string 37 | Content string 38 | Comments []comment 39 | CommentsCount int 40 | Level int 41 | URL string 42 | Dead bool 43 | } 44 | 45 | // CommentCardState is the state types for the CommentCard component 46 | type CommentCardState struct { 47 | ToggleOpen bool 48 | } 49 | 50 | // CommentCard creates instances of the CommentCard component 51 | func CommentCard(p CommentCardProps) *CommentCardElem { 52 | return buildCommentCardElem(CommentCardProps{ID: p.ID, User: p.User, Time: p.Time, TimeAgo: p.TimeAgo, Type: p.Type, Content: p.Content, Comments: p.Comments, CommentsCount: p.CommentsCount, Level: p.Level, URL: p.URL, Dead: p.Dead}) 53 | } 54 | 55 | // Equals is used to define component re-rendering 56 | func (c CommentCardState) Equals(v CommentCardState) bool { 57 | if c.ToggleOpen != v.ToggleOpen { 58 | return false 59 | } 60 | 61 | return true 62 | } 63 | 64 | // Equals is used to define component re-rendering 65 | func (c CommentCardProps) Equals(v CommentCardProps) bool { 66 | if c.ID != v.ID { 67 | return false 68 | } 69 | 70 | return true 71 | } 72 | 73 | // GetInitialState defines the initial state of the component 74 | func (f CommentCardDef) GetInitialState() CommentCardState { 75 | return CommentCardState{ 76 | ToggleOpen: true, 77 | } 78 | } 79 | 80 | // Render renders the component 81 | func (f CommentCardDef) Render() *react.LiElem { 82 | props := f.Props() 83 | 84 | var SubCommentsList *react.DivElem 85 | 86 | if props.CommentsCount > 0 { 87 | SubCommentsList = f.renderNestedComments(props.Comments) 88 | } 89 | return react.Li(&react.LiProps{ClassName: "comment"}, 90 | react.Div( 91 | &react.DivProps{ClassName: "by"}, 92 | react.A( 93 | &react.AProps{Href: "#/user/" + props.User}, 94 | react.S(props.User), 95 | ), 96 | react.S(" "+props.TimeAgo+" "), 97 | ), 98 | react.Div( 99 | &react.DivProps{ClassName: "text"}, 100 | react.Div(&react.DivProps{ 101 | DangerouslySetInnerHTML: react.NewDangerousInnerHTML(props.Content), 102 | }), 103 | ), 104 | SubCommentsList, 105 | ) 106 | } 107 | 108 | // RendersLi is used to define rendered component as a list item element 109 | func (f CommentCardDef) RendersLi(*react.LiElem) {} 110 | 111 | type toggleReplies struct{ CommentCardDef } 112 | 113 | // OnClick is used to define an onclick event for the component 114 | func (f CommentCardDef) OnClick(e *react.SyntheticMouseEvent) { 115 | newState := f.State() 116 | newState.ToggleOpen = !newState.ToggleOpen 117 | f.SetState(newState) 118 | } 119 | 120 | func (f CommentCardDef) renderNestedComments(nestedComments []comment) *react.DivElem { 121 | state := f.State() 122 | props := f.Props() 123 | 124 | var comments []react.RendersLi 125 | 126 | for _, comment := range nestedComments { 127 | comments = append(comments, CommentCard(CommentCardProps{ 128 | ID: comment.ID, 129 | User: comment.User, 130 | Time: comment.Time, 131 | TimeAgo: comment.TimeAgo, 132 | Type: comment.Type, 133 | Content: comment.Content, 134 | Comments: comment.Comments, 135 | CommentsCount: comment.CommentsCount, 136 | Level: comment.Level, 137 | URL: comment.URL, 138 | Dead: comment.Dead, 139 | })) 140 | } 141 | 142 | var content *react.DivElem 143 | 144 | if state.ToggleOpen { 145 | content = react.Div(nil, 146 | react.Div( 147 | &react.DivProps{ClassName: "toggle open"}, 148 | react.Span( 149 | &react.SpanProps{OnClick: toggleReplies{f}}, 150 | react.S("[-]"), 151 | ), 152 | ), 153 | react.Ul( 154 | &react.UlProps{ClassName: "comment-children"}, 155 | comments..., 156 | ), 157 | ) 158 | } else { 159 | content = react.Div(nil, 160 | react.Div( 161 | &react.DivProps{ClassName: "toggle"}, 162 | react.Span( 163 | &react.SpanProps{OnClick: toggleReplies{f}}, 164 | react.S("[+] "+strconv.Itoa(props.CommentsCount)+" replies collapsed"), 165 | ), 166 | ), 167 | ) 168 | } 169 | 170 | return content 171 | } 172 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "regexp" 23 | "strconv" 24 | "strings" 25 | 26 | "github.com/go-humble/router" 27 | "honnef.co/go/js/xhr" 28 | "myitcv.io/react" 29 | ) 30 | 31 | type story struct { 32 | ID int `json:"id"` 33 | Title string `json:"title"` 34 | Points int `json:"points"` 35 | User string `json:"user"` 36 | Time int `json:"time"` 37 | TimeAgo string `json:"time_ago"` 38 | CommentsCount int `json:"comments_count"` 39 | Type string `json:"type"` 40 | URL string `json:"url"` 41 | Domain string `json:"domain"` 42 | } 43 | 44 | type comment struct { 45 | ID int `json:"id"` 46 | User string `json:"user"` 47 | Time int `json:"time"` 48 | TimeAgo string `json:"time_ago"` 49 | Type string `json:"type"` 50 | Content string `json:"content"` 51 | Comments []comment `json:"comments"` 52 | CommentsCount int `json:"comments_count"` 53 | Level int `json:"level"` 54 | URL string `json:"url"` 55 | Dead bool `json:"dead,omitempty"` 56 | } 57 | 58 | type storyItem struct { 59 | ID int `json:"id"` 60 | Title string `json:"title"` 61 | Points int `json:"points"` 62 | User string `json:"user"` 63 | Time int `json:"time"` 64 | TimeAgo string `json:"time_ago"` 65 | Type string `json:"type"` 66 | Content string `json:"content"` 67 | Comments []comment `json:"comments"` 68 | CommentsCount int `json:"comments_count"` 69 | URL string `json:"url"` 70 | Domain string `json:"domain"` 71 | } 72 | 73 | type user struct { 74 | About string `json:"about"` 75 | CreatedTime int `json:"created_time"` 76 | Created string `json:"created"` 77 | ID string `json:"id"` 78 | Karma int `json:"karma"` 79 | } 80 | 81 | // AppDef is the definition for the App component 82 | type AppDef struct { 83 | react.ComponentDef 84 | } 85 | 86 | // AppState is the state types for the App component 87 | type AppState struct { 88 | Loading bool 89 | CurrRoute string 90 | CurrPage int 91 | Stories []story 92 | SelectedStory storyItem 93 | SelectedUser user 94 | } 95 | 96 | // App creates instances of the App component 97 | func App() *AppElem { 98 | return buildAppElem() 99 | } 100 | 101 | // GetInitialState defines the initial state of the component 102 | func (a AppDef) GetInitialState() AppState { 103 | return AppState{ 104 | Loading: false, 105 | CurrRoute: "news", 106 | CurrPage: 1, 107 | } 108 | } 109 | 110 | // Equals is used to define component re-rendering 111 | func (c AppState) Equals(v AppState) bool { 112 | if len(v.Stories) != len(c.Stories) { 113 | return false 114 | } 115 | 116 | for i := range v.Stories { 117 | if v.Stories[i] != c.Stories[i] { 118 | return false 119 | } 120 | } 121 | 122 | if v.Loading != c.Loading || v.CurrPage != c.CurrPage || v.CurrRoute != c.CurrRoute || v.SelectedStory.ID != c.SelectedStory.ID || v.SelectedUser.ID != c.SelectedUser.ID { 123 | return false 124 | } 125 | 126 | return true 127 | } 128 | 129 | // ComponentDidMount is the hook that fires as soon as the component finishes mounting 130 | func (a AppDef) ComponentDidMount() { 131 | Router := router.New() 132 | Router.ForceHashURL = true 133 | 134 | Router.HandleFunc("/", func(context *router.Context) { 135 | Router.Navigate("/news/1") 136 | }) 137 | Router.HandleFunc("/news/{page}", a.showStories) 138 | Router.HandleFunc("/newest/{page}", a.showStories) 139 | Router.HandleFunc("/show/{page}", a.showStories) 140 | Router.HandleFunc("/ask/{page}", a.showStories) 141 | Router.HandleFunc("/jobs/{page}", a.showStories) 142 | 143 | Router.HandleFunc("/item/{id}", a.showStory) 144 | 145 | Router.HandleFunc("/user/{id}", a.showUser) 146 | 147 | Router.Start() 148 | } 149 | 150 | func (a AppDef) showUser(context *router.Context) { 151 | re, _ := regexp.Compile("^(.*?/.*?)/") 152 | newState := a.State() 153 | path := re.FindString(context.Path) 154 | 155 | newState.Loading = true 156 | newState.CurrRoute = strings.Replace(path, "/", "", 2) 157 | a.SetState(newState) 158 | 159 | go func() { 160 | a.getUser(context.Params["id"]) 161 | }() 162 | } 163 | 164 | func (a AppDef) getUser(userID string) { 165 | newState := a.State() 166 | 167 | userChannel := make(chan user) 168 | go a.getUserRequest(userChannel, userID) 169 | newState.SelectedUser = <-userChannel 170 | newState.Loading = false 171 | 172 | a.SetState(newState) 173 | } 174 | 175 | func (a AppDef) getUserRequest(userChannel chan user, userID string) { 176 | url := "https://api.hnpwa.com/v0/user/" + userID + ".json" 177 | 178 | data, err := xhr.Send("GET", url, nil) 179 | if err != nil { 180 | println("Encountered error: ", err) 181 | } 182 | var user user 183 | json.NewDecoder(bytes.NewReader(data)).Decode(&user) 184 | 185 | userChannel <- user 186 | } 187 | 188 | func (a AppDef) showStory(context *router.Context) { 189 | re, _ := regexp.Compile("^(.*?/.*?)/") 190 | newState := a.State() 191 | path := re.FindString(context.Path) 192 | 193 | newState.Loading = true 194 | newState.CurrRoute = strings.Replace(path, "/", "", 2) 195 | a.SetState(newState) 196 | 197 | go func() { 198 | a.getStory(context.Params["id"]) 199 | }() 200 | } 201 | 202 | func (a AppDef) getStory(storyD string) { 203 | newState := a.State() 204 | 205 | storyChannel := make(chan storyItem) 206 | go a.getStoryRequest(storyChannel, storyD) 207 | newState.SelectedStory = <-storyChannel 208 | newState.Loading = false 209 | 210 | a.SetState(newState) 211 | } 212 | 213 | func (a AppDef) getStoryRequest(storyChannel chan storyItem, storyD string) { 214 | url := "https://api.hnpwa.com/v0/item/" + storyD + ".json" 215 | 216 | data, err := xhr.Send("GET", url, nil) 217 | if err != nil { 218 | println("Encountered error: ", err) 219 | } 220 | var story storyItem 221 | json.NewDecoder(strings.NewReader(string(data))).Decode(&story) 222 | storyChannel <- story 223 | } 224 | 225 | func (a AppDef) showStories(context *router.Context) { 226 | re, _ := regexp.Compile("^(.*?/.*?)/") 227 | newState := a.State() 228 | path := re.FindString(context.Path) 229 | 230 | currPage, err := strconv.Atoi(context.Params["page"]) 231 | 232 | if err != nil { 233 | print("Something went wrong!") 234 | } 235 | 236 | newState.Loading = true 237 | newState.CurrPage = currPage 238 | newState.CurrRoute = strings.Replace(path, "/", "", 2) 239 | 240 | a.SetState(newState) 241 | 242 | go func() { 243 | a.getStories(path, context.Params["page"]) 244 | }() 245 | } 246 | 247 | func (a AppDef) getStories(storyType string, pageNum string) { 248 | newState := a.State() 249 | 250 | storiesChannel := make(chan []story) 251 | go a.getStoriesRequest(storiesChannel, storyType, pageNum) 252 | newState.Stories = <-storiesChannel 253 | newState.Loading = false 254 | 255 | a.SetState(newState) 256 | } 257 | 258 | func (a AppDef) getStoriesRequest(storiesChannel chan []story, storyType string, pageNum string) { 259 | url := "https://api.hnpwa.com/v0" + storyType + pageNum + ".json" 260 | 261 | data, err := xhr.Send("GET", url, nil) 262 | if err != nil { 263 | println("Encountered error: ", err) 264 | } 265 | var stories []story 266 | json.NewDecoder(strings.NewReader(string(data))).Decode(&stories) 267 | storiesChannel <- stories 268 | } 269 | 270 | func (a AppDef) renderLoader() react.Element { 271 | return react.Div(&react.DivProps{ClassName: "loader-container"}, 272 | react.Div(&react.DivProps{ClassName: "loader"}, 273 | react.Div(nil), 274 | react.Div(nil), 275 | react.Div(nil), 276 | react.Div(nil), 277 | ), 278 | ) 279 | } 280 | 281 | // Render renders the component 282 | func (a AppDef) Render() react.Element { 283 | state := a.State() 284 | 285 | var content react.Element 286 | 287 | if state.Loading { 288 | if state.CurrRoute != "item" && state.CurrRoute != "user" { 289 | content = react.Fragment(nil, 290 | PageNav(PageNavProps{CurrPage: state.CurrPage, StoryType: state.CurrRoute, NumStories: len(state.Stories)}), 291 | react.Div(nil, 292 | react.Div(&react.DivProps{ClassName: "wrapper"}, 293 | react.Div(&react.DivProps{ClassName: "story-list view"}, 294 | react.Ul( 295 | &react.UlProps{ClassName: "skeleton"}, 296 | ), 297 | ), 298 | ), 299 | ), 300 | ) 301 | } else { 302 | content = a.renderLoader() 303 | } 304 | } else if state.CurrRoute == "item" { 305 | content = Story(StoryProps{ 306 | ID: state.SelectedStory.ID, 307 | Title: state.SelectedStory.Title, 308 | Points: state.SelectedStory.Points, 309 | User: state.SelectedStory.User, 310 | Time: state.SelectedStory.Time, 311 | TimeAgo: state.SelectedStory.TimeAgo, 312 | Type: state.SelectedStory.Type, 313 | Content: state.SelectedStory.Content, 314 | Comments: state.SelectedStory.Comments, 315 | CommentsCount: state.SelectedStory.CommentsCount, 316 | URL: state.SelectedStory.URL, 317 | Domain: state.SelectedStory.Domain, 318 | }) 319 | } else if state.CurrRoute == "user" { 320 | content = User(UserProps{ 321 | About: state.SelectedUser.About, 322 | CreatedTime: state.SelectedUser.CreatedTime, 323 | Created: state.SelectedUser.Created, 324 | ID: state.SelectedUser.ID, 325 | Karma: state.SelectedUser.Karma, 326 | }) 327 | } else { 328 | content = react.Div(nil, 329 | PageNav(PageNavProps{CurrPage: state.CurrPage, StoryType: state.CurrRoute, NumStories: len(state.Stories)}), 330 | StoryList(StoryListProps{StoryItems: state.Stories}), 331 | ) 332 | } 333 | 334 | return react.Div(nil, 335 | Header(HeaderProps{path: state.CurrRoute}), 336 | content, 337 | ) 338 | } 339 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright 2018 Google Inc. 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. --------------------------------------------------------------------------------