<|start_filename|>story.go<|end_filename|>
/**
 * Copyright 2018 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package main

import (
	"strconv"

	"myitcv.io/react"
)

// StoryDef is the definition for the Story component
type StoryDef struct {
	react.ComponentDef
}

// StoryProps is the prop types for the Story component
type StoryProps struct {
	ID            int
	Title         string
	Points        int
	User          string
	Time          int
	TimeAgo       string
	Type          string
	Content       string
	Comments      []comment
	CommentsCount int
	URL           string
	Domain        string
}

// Story creates instances of the Story component
func Story(p StoryProps) *StoryElem {
	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})
}

// Equals is used to define component re-rendering
func (c StoryProps) Equals(v StoryProps) bool {
	if c.ID != v.ID {
		return false
	}

	return true
}

// Render renders the Story component
func (f StoryDef) Render() react.Element {
	props := f.Props()

	var comments []react.RendersLi

	if len(props.Comments) > 0 {
		for _, comment := range props.Comments {
			comments = append(comments, CommentCard(CommentCardProps{
				ID:            comment.ID,
				User:          comment.User,
				Time:          comment.Time,
				TimeAgo:       comment.TimeAgo,
				Type:          comment.Type,
				Content:       comment.Content,
				Comments:      comment.Comments,
				CommentsCount: comment.CommentsCount,
				Level:         comment.Level,
				URL:           comment.URL,
				Dead:          comment.Dead,
			}))
		}
	}

	domainStr := ""
	if props.Domain != "" {
		domainStr = "(" + props.Domain + ")"
	}

	return react.Div(nil,
		react.Div(&react.DivProps{ClassName: "wrapper"},
			react.Div(&react.DivProps{ClassName: "view"},
				react.Div(&react.DivProps{ClassName: "item-view-header"},
					react.A(
						&react.AProps{Target: "_blank", Href: props.URL, ClassName: "github"},
						react.H1(
							nil,
							react.S(props.Title),
						),
					),
					react.Span(
						&react.SpanProps{ClassName: "host"},
						react.S(domainStr),
					),
					react.P(
						&react.PProps{ClassName: "meta"},
						react.S(strconv.Itoa(props.Points)+" points | by "),
						react.A(
							&react.AProps{Href: "#/user/" + props.User},
							react.S(props.User),
						),
						react.S(" "+props.TimeAgo),
					),
				),
				react.Div(&react.DivProps{ClassName: "item-view-comments"},
					react.P(&react.PProps{ClassName: "item-view-comments-header"},
						react.S(strconv.Itoa(props.CommentsCount)+" comments"),
					),
					react.Ul(
						&react.UlProps{ClassName: "comment-children"},
						comments...,
					),
				),
			),
		),
	)
}


<|start_filename|>storyCard.go<|end_filename|>
/**
 * Copyright 2018 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package main

import (
	"strconv"
	"strings"

	"myitcv.io/react"
)

// StoryCardDef is the definition for the StoryCard component
type StoryCardDef struct {
	react.ComponentDef
}

// StoryCardProps is the prop types for the StoryCard component
type StoryCardProps struct {
	ID            int
	title         string
	points        int
	commentsCount int
	domain        string
	timeAgo       string
	user          string
	storyType     string
	URL           string
}

// StoryCard creates instances of the StoryCard component
func StoryCard(s StoryCardProps) *StoryCardElem {
	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})
}

// Render renders the StoryCard component
func (f StoryCardDef) Render() *react.LiElem {
	props := f.Props()

	var userSpan react.Element
	if props.user == "" {
		userSpan = nil
	} else {
		userSpan = react.Span(
			&react.SpanProps{ClassName: "by"},
			react.S(" by "),
			react.A(
				&react.AProps{Href: "#/user/" + props.user},
				react.S(" "+props.user),
			),
		)
	}

	var commentsSpan react.Element
	if props.storyType == "job" {
		commentsSpan = nil
	} else {
		commentsSpan = react.Span(
			&react.SpanProps{ClassName: "comments-link"},
			react.S(" | "),
			react.A(
				&react.AProps{Href: "#/item/" + strconv.Itoa(props.ID)},
				react.S(strconv.Itoa(props.commentsCount)+" comments"),
			),
		)
	}

	link := props.URL
	if strings.HasPrefix(props.URL, "item") {
		link = "#/item/" + strconv.Itoa(props.ID)
	}

	domainStr := ""
	if props.domain != "" {
		domainStr = " (" + props.domain + ")"
	}

	return react.Li(&react.LiProps{ClassName: "story-card"},
		react.Span(
			&react.SpanProps{ClassName: "score"},
			react.S(strconv.Itoa(props.points)),
		),
		react.Span(
			&react.SpanProps{ClassName: "title"},
			react.A(
				&react.AProps{Href: link},
				react.S(props.title),
			),
			react.Span(
				&react.SpanProps{ClassName: "host"},
				react.S(domainStr),
			),
		),
		react.Br(nil, nil),
		react.Span(
			&react.SpanProps{ClassName: "meta"},
			userSpan,
			react.Span(
				&react.SpanProps{ClassName: "time"},
				react.S(" "+props.timeAgo+" "),
			),
			commentsSpan,
		),
	)
}

// RendersLi is used to return the rendered StoryCard component as a list item element
func (f StoryCardDef) RendersLi(*react.LiElem) {}


<|start_filename|>pageNav.go<|end_filename|>
/**
 * Copyright 2018 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package main

import (
	"strconv"

	"myitcv.io/react"
)

// PageNavDef is the definition for the PageNav component
type PageNavDef struct {
	react.ComponentDef
}

// PageNavProps is the prop types for the PageNav component
type PageNavProps struct {
	CurrPage   int
	StoryType  string
	NumStories int
}

// PageNav creates instances of the PageNav component
func PageNav(p PageNavProps) *PageNavElem {
	return buildPageNavElem(PageNavProps{CurrPage: p.CurrPage, StoryType: p.StoryType, NumStories: p.NumStories})
}

// Render renders the PageNav component
func (f PageNavDef) Render() react.Element {
	props := f.Props()

	var prevLink react.Element
	var nextLink react.Element

	if props.CurrPage == 1 {
		prevLink = react.Span(
			&react.SpanProps{ClassName: "disabled"},
			react.S("< prev"),
		)
	} else {
		prevLink = react.A(
			&react.AProps{Href: "/#/" + props.StoryType + "/" + strconv.Itoa(props.CurrPage-1)},
			react.S("< prev"),
		)
	}

	if props.NumStories == 30 {
		nextLink = react.A(
			&react.AProps{Href: "/#/" + props.StoryType + "/" + strconv.Itoa(props.CurrPage+1)},
			react.S("more >"),
		)
	} else {
		nextLink = react.Span(
			&react.SpanProps{ClassName: "disabled"},
			react.S("more >"),
		)
	}

	return react.Div(&react.DivProps{ClassName: "page-nav-container"},
		react.Div(&react.DivProps{ClassName: "page-nav"},
			prevLink,
			react.Span(
				nil,
				react.S(react.S(strconv.Itoa(props.CurrPage))),
			),
			nextLink,
		),
	)
}


<|start_filename|>commentCard.go<|end_filename|>
/**
 * Copyright 2018 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package main

import (
	"strconv"

	"myitcv.io/react"
)

// CommentCardDef is the definition for the CommentCard component
type CommentCardDef struct {
	react.ComponentDef
}

// CommentCardProps is the prop types for the CommentCard component
type CommentCardProps struct {
	ID            int
	User          string
	Time          int
	TimeAgo       string
	Type          string
	Content       string
	Comments      []comment
	CommentsCount int
	Level         int
	URL           string
	Dead          bool
}

// CommentCardState is the state types for the CommentCard component
type CommentCardState struct {
	ToggleOpen bool
}

// CommentCard creates instances of the CommentCard component
func CommentCard(p CommentCardProps) *CommentCardElem {
	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})
}

// Equals is used to define component re-rendering
func (c CommentCardState) Equals(v CommentCardState) bool {
	if c.ToggleOpen != v.ToggleOpen {
		return false
	}

	return true
}

// Equals is used to define component re-rendering
func (c CommentCardProps) Equals(v CommentCardProps) bool {
	if c.ID != v.ID {
		return false
	}

	return true
}

// GetInitialState defines the initial state of the component
func (f CommentCardDef) GetInitialState() CommentCardState {
	return CommentCardState{
		ToggleOpen: true,
	}
}

// Render renders the component
func (f CommentCardDef) Render() *react.LiElem {
	props := f.Props()

	var SubCommentsList *react.DivElem

	if props.CommentsCount > 0 {
		SubCommentsList = f.renderNestedComments(props.Comments)
	}
	return react.Li(&react.LiProps{ClassName: "comment"},
		react.Div(
			&react.DivProps{ClassName: "by"},
			react.A(
				&react.AProps{Href: "#/user/" + props.User},
				react.S(props.User),
			),
			react.S(" "+props.TimeAgo+" "),
		),
		react.Div(
			&react.DivProps{ClassName: "text"},
			react.Div(&react.DivProps{
				DangerouslySetInnerHTML: react.NewDangerousInnerHTML(props.Content),
			}),
		),
		SubCommentsList,
	)
}

// RendersLi is used to define rendered component as a list item element
func (f CommentCardDef) RendersLi(*react.LiElem) {}

type toggleReplies struct{ CommentCardDef }

// OnClick is used to define an onclick event for the component
func (f CommentCardDef) OnClick(e *react.SyntheticMouseEvent) {
	newState := f.State()
	newState.ToggleOpen = !newState.ToggleOpen
	f.SetState(newState)
}

func (f CommentCardDef) renderNestedComments(nestedComments []comment) *react.DivElem {
	state := f.State()
	props := f.Props()

	var comments []react.RendersLi

	for _, comment := range nestedComments {
		comments = append(comments, CommentCard(CommentCardProps{
			ID:            comment.ID,
			User:          comment.User,
			Time:          comment.Time,
			TimeAgo:       comment.TimeAgo,
			Type:          comment.Type,
			Content:       comment.Content,
			Comments:      comment.Comments,
			CommentsCount: comment.CommentsCount,
			Level:         comment.Level,
			URL:           comment.URL,
			Dead:          comment.Dead,
		}))
	}

	var content *react.DivElem

	if state.ToggleOpen {
		content = react.Div(nil,
			react.Div(
				&react.DivProps{ClassName: "toggle open"},
				react.Span(
					&react.SpanProps{OnClick: toggleReplies{f}},
					react.S("[-]"),
				),
			),
			react.Ul(
				&react.UlProps{ClassName: "comment-children"},
				comments...,
			),
		)
	} else {
		content = react.Div(nil,
			react.Div(
				&react.DivProps{ClassName: "toggle"},
				react.Span(
					&react.SpanProps{OnClick: toggleReplies{f}},
					react.S("[+] "+strconv.Itoa(props.CommentsCount)+" replies collapsed"),
				),
			),
		)
	}

	return content
}


<|start_filename|>storyList.go<|end_filename|>
/**
 * Copyright 2018 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package main

import (
	"myitcv.io/react"
)

// StoryListDef is the definition for the StoryList component
type StoryListDef struct {
	react.ComponentDef
}

// StoryListProps is the prop types for the StoryList component
type StoryListProps struct {
	StoryItems []story
}

// StoryList creates instances of the StoryList component
func StoryList(p StoryListProps) *StoryListElem {
	return buildStoryListElem(StoryListProps{StoryItems: p.StoryItems})
}

// Equals is used to define component re-rendering
func (c StoryListProps) Equals(v StoryListProps) bool {
	if len(v.StoryItems) != len(c.StoryItems) {
		return false
	}

	for i := range v.StoryItems {
		if v.StoryItems[i] != c.StoryItems[i] {
			return false
		}
	}

	return true
}

// Render renders the StoryList component
func (f StoryListDef) Render() react.Element {
	props := f.Props()

	var storyItems []react.RendersLi
	var storyList []react.RendersLi

	if len(props.StoryItems) > 0 {
		for _, story := range props.StoryItems {
			storyItems = append(storyItems, StoryCard(StoryCardProps{
				ID:            story.ID,
				title:         story.Title,
				points:        story.Points,
				commentsCount: story.CommentsCount,
				domain:        story.Domain,
				timeAgo:       story.TimeAgo,
				user:          story.User,
				URL:           story.URL,
				storyType:     story.Type,
			}))
		}

		storyList = storyItems
	} else {
		storyList = append(storyList, react.Li(&react.LiProps{ClassName: "story-card"},
			react.Span(
				&react.SpanProps{ClassName: "title"},
				react.S("No more story items!")),
		))
	}

	return react.Div(nil,
		react.Div(&react.DivProps{ClassName: "wrapper"},
			react.Div(&react.DivProps{ClassName: "story-list view"},
				react.Ul(
					nil,
					storyList...,
				),
			),
		),
	)
}


<|start_filename|>workbox-config.js<|end_filename|>
/**
 * Copyright 2018 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

module.exports = {
  "globDirectory": "build/",
  "globPatterns": [
    "**/*.{svg,html,js}"
  ],
  "swDest": "build/service-worker.js",
  "maximumFileSizeToCacheInBytes": 3 * 1024 * 1024
};

<|start_filename|>user.go<|end_filename|>
/**
 * Copyright 2018 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package main

import (
	"strconv"

	"myitcv.io/react"
)

// UserDef is the definition for the User component
type UserDef struct {
	react.ComponentDef
}

// UserProps is the prop types for the User component
type UserProps struct {
	About       string
	CreatedTime int
	Created     string
	ID          string
	Karma       int
}

// User creates instances of the User component
func User(p UserProps) *UserElem {
	return buildUserElem(UserProps{ID: p.ID, CreatedTime: p.CreatedTime, Created: p.Created, Karma: p.Karma, About: p.About})
}

// Render renders the User component
func (f UserDef) Render() react.Element {
	props := f.Props()

	return react.Div(nil,
		react.Div(&react.DivProps{ClassName: "wrapper"},
			react.Div(&react.DivProps{ClassName: "user-view view"},
				react.H1(
					nil,
					react.S("User : "+props.ID),
				),
				react.Ul(
					&react.UlProps{ClassName: "meta"},
					react.Li(
						nil,
						react.Span(
							&react.SpanProps{ClassName: "label"},
							react.S("Created:"),
						),
						react.S(" "+props.Created),
					),
					react.Li(
						nil,
						react.Span(
							&react.SpanProps{ClassName: "label"},
							react.S("Karma:"),
						),
						react.S(" "+strconv.Itoa(props.Karma)),
					),
					react.Li(
						&react.LiProps{ClassName: "about", DangerouslySetInnerHTML: react.NewDangerousInnerHTML(props.About)},
					),
				),
				react.P(
					&react.PProps{ClassName: "links"},
					react.A(
						&react.AProps{Target: "_blank", Href: "https://news.ycombinator.com/submitted?id=" + props.ID},
						react.S("submissions"),
					),
					react.S("  |  "),
					react.A(
						&react.AProps{Target: "_blank", Href: "https://news.ycombinator.com/threads?id=" + props.ID},
						react.S("comments"),
					),
				),
			),
		),
	)
}


<|start_filename|>package.json<|end_filename|>
{
	"private": true,
	"name": "go-hacker-news",
	"version": "0.0.0",
	"license": "MIT",
	"scripts": {
		"build": "gopherjs build --output build/script.min.js --minify && npm run sw",
		"sw": "workbox generateSW workbox-config.js"
	},
	"devDependencies": {
		"workbox-cli": "^3.6.2"
	},
	"dependencies": {}
}


<|start_filename|>app.go<|end_filename|>
/**
 * Copyright 2018 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package main

import (
	"bytes"
	"encoding/json"
	"regexp"
	"strconv"
	"strings"

	"github.com/go-humble/router"
	"honnef.co/go/js/xhr"
	"myitcv.io/react"
)

type story struct {
	ID            int    `json:"id"`
	Title         string `json:"title"`
	Points        int    `json:"points"`
	User          string `json:"user"`
	Time          int    `json:"time"`
	TimeAgo       string `json:"time_ago"`
	CommentsCount int    `json:"comments_count"`
	Type          string `json:"type"`
	URL           string `json:"url"`
	Domain        string `json:"domain"`
}

type comment struct {
	ID            int       `json:"id"`
	User          string    `json:"user"`
	Time          int       `json:"time"`
	TimeAgo       string    `json:"time_ago"`
	Type          string    `json:"type"`
	Content       string    `json:"content"`
	Comments      []comment `json:"comments"`
	CommentsCount int       `json:"comments_count"`
	Level         int       `json:"level"`
	URL           string    `json:"url"`
	Dead          bool      `json:"dead,omitempty"`
}

type storyItem struct {
	ID            int       `json:"id"`
	Title         string    `json:"title"`
	Points        int       `json:"points"`
	User          string    `json:"user"`
	Time          int       `json:"time"`
	TimeAgo       string    `json:"time_ago"`
	Type          string    `json:"type"`
	Content       string    `json:"content"`
	Comments      []comment `json:"comments"`
	CommentsCount int       `json:"comments_count"`
	URL           string    `json:"url"`
	Domain        string    `json:"domain"`
}

type user struct {
	About       string `json:"about"`
	CreatedTime int    `json:"created_time"`
	Created     string `json:"created"`
	ID          string `json:"id"`
	Karma       int    `json:"karma"`
}

// AppDef is the definition for the App component
type AppDef struct {
	react.ComponentDef
}

// AppState is the state types for the App component
type AppState struct {
	Loading       bool
	CurrRoute     string
	CurrPage      int
	Stories       []story
	SelectedStory storyItem
	SelectedUser  user
}

// App creates instances of the App component
func App() *AppElem {
	return buildAppElem()
}

// GetInitialState defines the initial state of the component
func (a AppDef) GetInitialState() AppState {
	return AppState{
		Loading:   false,
		CurrRoute: "news",
		CurrPage:  1,
	}
}

// Equals is used to define component re-rendering
func (c AppState) Equals(v AppState) bool {
	if len(v.Stories) != len(c.Stories) {
		return false
	}

	for i := range v.Stories {
		if v.Stories[i] != c.Stories[i] {
			return false
		}
	}

	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 {
		return false
	}

	return true
}

// ComponentDidMount is the hook that fires as soon as the component finishes mounting
func (a AppDef) ComponentDidMount() {
	Router := router.New()
	Router.ForceHashURL = true

	Router.HandleFunc("/", func(context *router.Context) {
		Router.Navigate("/news/1")
	})
	Router.HandleFunc("/news/{page}", a.showStories)
	Router.HandleFunc("/newest/{page}", a.showStories)
	Router.HandleFunc("/show/{page}", a.showStories)
	Router.HandleFunc("/ask/{page}", a.showStories)
	Router.HandleFunc("/jobs/{page}", a.showStories)

	Router.HandleFunc("/item/{id}", a.showStory)

	Router.HandleFunc("/user/{id}", a.showUser)

	Router.Start()
}

func (a AppDef) showUser(context *router.Context) {
	re, _ := regexp.Compile("^(.*?/.*?)/")
	newState := a.State()
	path := re.FindString(context.Path)

	newState.Loading = true
	newState.CurrRoute = strings.Replace(path, "/", "", 2)
	a.SetState(newState)

	go func() {
		a.getUser(context.Params["id"])
	}()
}

func (a AppDef) getUser(userID string) {
	newState := a.State()

	userChannel := make(chan user)
	go a.getUserRequest(userChannel, userID)
	newState.SelectedUser = <-userChannel
	newState.Loading = false

	a.SetState(newState)
}

func (a AppDef) getUserRequest(userChannel chan user, userID string) {
	url := "https://api.hnpwa.com/v0/user/" + userID + ".json"

	data, err := xhr.Send("GET", url, nil)
	if err != nil {
		println("Encountered error: ", err)
	}
	var user user
	json.NewDecoder(bytes.NewReader(data)).Decode(&user)

	userChannel <- user
}

func (a AppDef) showStory(context *router.Context) {
	re, _ := regexp.Compile("^(.*?/.*?)/")
	newState := a.State()
	path := re.FindString(context.Path)

	newState.Loading = true
	newState.CurrRoute = strings.Replace(path, "/", "", 2)
	a.SetState(newState)

	go func() {
		a.getStory(context.Params["id"])
	}()
}

func (a AppDef) getStory(storyD string) {
	newState := a.State()

	storyChannel := make(chan storyItem)
	go a.getStoryRequest(storyChannel, storyD)
	newState.SelectedStory = <-storyChannel
	newState.Loading = false

	a.SetState(newState)
}

func (a AppDef) getStoryRequest(storyChannel chan storyItem, storyD string) {
	url := "https://api.hnpwa.com/v0/item/" + storyD + ".json"

	data, err := xhr.Send("GET", url, nil)
	if err != nil {
		println("Encountered error: ", err)
	}
	var story storyItem
	json.NewDecoder(strings.NewReader(string(data))).Decode(&story)
	storyChannel <- story
}

func (a AppDef) showStories(context *router.Context) {
	re, _ := regexp.Compile("^(.*?/.*?)/")
	newState := a.State()
	path := re.FindString(context.Path)

	currPage, err := strconv.Atoi(context.Params["page"])

	if err != nil {
		print("Something went wrong!")
	}

	newState.Loading = true
	newState.CurrPage = currPage
	newState.CurrRoute = strings.Replace(path, "/", "", 2)

	a.SetState(newState)

	go func() {
		a.getStories(path, context.Params["page"])
	}()
}

func (a AppDef) getStories(storyType string, pageNum string) {
	newState := a.State()

	storiesChannel := make(chan []story)
	go a.getStoriesRequest(storiesChannel, storyType, pageNum)
	newState.Stories = <-storiesChannel
	newState.Loading = false

	a.SetState(newState)
}

func (a AppDef) getStoriesRequest(storiesChannel chan []story, storyType string, pageNum string) {
	url := "https://api.hnpwa.com/v0" + storyType + pageNum + ".json"

	data, err := xhr.Send("GET", url, nil)
	if err != nil {
		println("Encountered error: ", err)
	}
	var stories []story
	json.NewDecoder(strings.NewReader(string(data))).Decode(&stories)
	storiesChannel <- stories
}

func (a AppDef) renderLoader() react.Element {
	return react.Div(&react.DivProps{ClassName: "loader-container"},
		react.Div(&react.DivProps{ClassName: "loader"},
			react.Div(nil),
			react.Div(nil),
			react.Div(nil),
			react.Div(nil),
		),
	)
}

// Render renders the component
func (a AppDef) Render() react.Element {
	state := a.State()

	var content react.Element

	if state.Loading {
		if state.CurrRoute != "item" && state.CurrRoute != "user" {
			content = react.Fragment(nil,
				PageNav(PageNavProps{CurrPage: state.CurrPage, StoryType: state.CurrRoute, NumStories: len(state.Stories)}),
				react.Div(nil,
					react.Div(&react.DivProps{ClassName: "wrapper"},
						react.Div(&react.DivProps{ClassName: "story-list view"},
							react.Ul(
								&react.UlProps{ClassName: "skeleton"},
							),
						),
					),
				),
			)
		} else {
			content = a.renderLoader()
		}
	} else if state.CurrRoute == "item" {
		content = Story(StoryProps{
			ID:            state.SelectedStory.ID,
			Title:         state.SelectedStory.Title,
			Points:        state.SelectedStory.Points,
			User:          state.SelectedStory.User,
			Time:          state.SelectedStory.Time,
			TimeAgo:       state.SelectedStory.TimeAgo,
			Type:          state.SelectedStory.Type,
			Content:       state.SelectedStory.Content,
			Comments:      state.SelectedStory.Comments,
			CommentsCount: state.SelectedStory.CommentsCount,
			URL:           state.SelectedStory.URL,
			Domain:        state.SelectedStory.Domain,
		})
	} else if state.CurrRoute == "user" {
		content = User(UserProps{
			About:       state.SelectedUser.About,
			CreatedTime: state.SelectedUser.CreatedTime,
			Created:     state.SelectedUser.Created,
			ID:          state.SelectedUser.ID,
			Karma:       state.SelectedUser.Karma,
		})
	} else {
		content = react.Div(nil,
			PageNav(PageNavProps{CurrPage: state.CurrPage, StoryType: state.CurrRoute, NumStories: len(state.Stories)}),
			StoryList(StoryListProps{StoryItems: state.Stories}),
		)
	}

	return react.Div(nil,
		Header(HeaderProps{path: state.CurrRoute}),
		content,
	)
}
