diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..e7a52bd --- /dev/null +++ b/LICENCE @@ -0,0 +1,22 @@ +MIT Licence + +Copyright (c) 2019 Ayooluwa Isaiah + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..84de017 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: bin/news-demo -apikey $NEWS_API_KEY diff --git a/README.md b/README.md new file mode 100644 index 0000000..13ba2e9 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# News Demo Application + +Learn web development with Go. Live demo: https://freshman-news.herokuapp.com/ + +Here's what the completed application looks like: + +![demo](https://res.cloudinary.com/freshman/image/upload/v1566482694/Screenshot_from_2019-08-22_15-04-27.png) + +The code in this repo is meant to be a reference point for anyone following along with the [tutorial](https://freshman.tech/web-development-with-go/). + +## Prerequisites + +- You need to have Go installed on your computer. The version used for the tutorial is **1.12.9**. + +- Sign up for a [News API account](https://newsapi.org/register) and get your free API key. + +## Get started + +- Clone this repository to your filesystem. + +- `cd` into it and run the following command: `go run main.go -apikey=` to start the server on port 3000. + +- View http://localhost:3000 in your browser. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e00c9c9 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/freshman-tech/news-demo + +go 1.12.9 diff --git a/index.html b/index.html index 408ff9f..81520f8 100644 --- a/index.html +++ b/index.html @@ -8,5 +8,51 @@ +
+
+ +
+ +
+ View on Github +
+
+
+ {{ if (gt .Results.TotalResults 0)}} +

About {{ .Results.TotalResults }} results were found. You are on page {{ .CurrentPage }} of {{ .TotalPages }}.

+ {{ else if (ne .SearchKey "") and (eq .Results.TotalResults 0) }} +

No results found for your query: {{ .SearchKey }}.

+ {{ end }} +
+ +
    + {{ range .Results.Articles }} +
  • +
    + +

    {{.Title }}

    +
    +

    {{ .Description }}

    + +
    + +
  • + {{ end }} +
+ + +
+
diff --git a/main.go b/main.go index 8b13789..fe1d38c 100644 --- a/main.go +++ b/main.go @@ -1 +1,153 @@ +package main +import ( + "encoding/json" + "flag" + "fmt" + "html/template" + "log" + "math" + "net/http" + "net/url" + "os" + "strconv" + "time" +) + +var tpl = template.Must(template.ParseFiles("index.html")) +var apiKey *string + +type Source struct { + ID interface{} `json:"id"` + Name string `json:"name"` +} + +type Article struct { + Source Source `json:"source"` + Author string `json:"author"` + Title string `json:"title"` + Description string `json:"description"` + URL string `json:"url"` + URLToImage string `json:"urlToImage"` + PublishedAt time.Time `json:"publishedAt"` + Content string `json:"content"` +} + +func (a *Article) FormatPublishedDate() string { + year, month, day := a.PublishedAt.Date() + return fmt.Sprintf("%v %d, %d", month, day, year) +} + +type Results struct { + Status string `json:"status"` + TotalResults int `json:"totalResults"` + Articles []Article `json:"articles"` +} + +type Search struct { + SearchKey string + NextPage int + TotalPages int + Results Results +} + +func (s *Search) IsLastPage() bool { + return s.NextPage >= s.TotalPages +} + +func (s *Search) PreviousPage() int { + return s.CurrentPage() - 1 +} + +func (s *Search) CurrentPage() int { + if s.NextPage == 1 { + return s.NextPage + } + + return s.NextPage - 1 +} + +func indexHandler(w http.ResponseWriter, r *http.Request) { + tpl.Execute(w, nil) +} + +func searchHandler(w http.ResponseWriter, r *http.Request) { + u, err := url.Parse(r.URL.String()) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal server error")) + return + } + + params := u.Query() + searchKey := params.Get("q") + page := params.Get("page") + if page == "" { + page = "1" + } + + search := &Search{} + search.SearchKey = searchKey + + next, err := strconv.Atoi(page) + if err != nil { + http.Error(w, "Unexpected server error", http.StatusInternalServerError) + return + } + + search.NextPage = next + pageSize := 20 + + endpoint := fmt.Sprintf("https://newsapi.org/v2/everything?q=%s&pageSize=%d&page=%d&apiKey=%s&sortBy=publishedAt&language=en", url.QueryEscape(search.SearchKey), pageSize, search.NextPage, *apiKey) + resp, err := http.Get(endpoint) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + defer resp.Body.Close() + + if resp.StatusCode != 200 { + w.WriteHeader(http.StatusInternalServerError) + return + } + + err = json.NewDecoder(resp.Body).Decode(&search.Results) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + search.TotalPages = int(math.Ceil(float64(search.Results.TotalResults / pageSize))) + if ok := !search.IsLastPage(); ok { + search.NextPage++ + } + + err = tpl.Execute(w, search) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + } +} + +func main() { + port := os.Getenv("PORT") + if port == "" { + port = "3000" + } + + apiKey = flag.String("apikey", "", "Newsapi.org access key") + flag.Parse() + + if *apiKey == "" { + log.Fatal("apiKey must be set") + } + + mux := http.NewServeMux() + + fs := http.FileServer(http.Dir("assets")) + mux.Handle("/assets/", http.StripPrefix("/assets/", fs)) + + mux.HandleFunc("/search", searchHandler) + mux.HandleFunc("/", indexHandler) + http.ListenAndServe(":"+port, mux) +}