Initial commit

This commit is contained in:
Ayo Isaiah
2019-08-22 15:20:14 +01:00
parent dcc89a85b7
commit 3f47fb7c20
6 changed files with 247 additions and 0 deletions

22
LICENCE Normal file
View File

@@ -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.

1
Procfile Normal file
View File

@@ -0,0 +1 @@
web: bin/news-demo -apikey $NEWS_API_KEY

23
README.md Normal file
View File

@@ -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=<your news api key>` to start the server on port 3000.
- View http://localhost:3000 in your browser.

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/freshman-tech/news-demo
go 1.12.9

View File

@@ -8,5 +8,51 @@
<link rel="stylesheet" href="/assets/style.css">
</head>
<body>
<main>
<header>
<a class="logo" href="/">News Demo</a>
<form action="/search" method="GET">
<input autofocus class="search-input" value="{{ .SearchKey }}" placeholder="Enter a news topic" type="search" name="q">
</form>
<a href="https://github.com/freshman-tech/news" class="button
github-button">View on Github</a>
</header>
<section class="container">
<div class="result-count">
{{ if (gt .Results.TotalResults 0)}}
<p>About <strong>{{ .Results.TotalResults }}</strong> results were found. You are on page <strong>{{ .CurrentPage }}</strong> of <strong> {{ .TotalPages }}</strong>.</p>
{{ else if (ne .SearchKey "") and (eq .Results.TotalResults 0) }}
<p>No results found for your query: <strong>{{ .SearchKey }}</strong>.</p>
{{ end }}
</div>
<ul class="search-results">
{{ range .Results.Articles }}
<li class="news-article">
<div>
<a target="_blank" rel="noreferrer noopener" href="{{.URL}}">
<h3 class="title">{{.Title }}</h3>
</a>
<p class="description">{{ .Description }}</p>
<div class="metadata">
<p class="source">{{ .Source.Name }}</p>
<time class="published-date">{{ .FormatPublishedDate }}</time>
</div>
</div>
<img class="article-image" src="{{ .URLToImage }}">
</li>
{{ end }}
</ul>
<div class="pagination">
{{ if (gt .NextPage 2) }}
<a href="/search?q={{ .SearchKey }}&page={{ .PreviousPage }}" class="button previous-page">Previous</a>
{{ end }}
{{ if (ne .IsLastPage true) }}
<a href="/search?q={{ .SearchKey }}&page={{ .NextPage }}" class="button next-page">Next</a>
{{ end }}
</div>
</section>
</main>
</body>
</html>

152
main.go
View File

@@ -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)
}