Update code
This commit is contained in:
4
go.mod
4
go.mod
@@ -1,3 +1,5 @@
|
||||
module github.com/freshman-tech/news-demo
|
||||
|
||||
go 1.14
|
||||
go 1.15
|
||||
|
||||
require github.com/joho/godotenv v1.3.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1,2 +1,2 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
|
||||
112
index.html
112
index.html
@@ -1,33 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>News App Demo</title>
|
||||
<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-demo" 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 and (ne .SearchKey "") (eq .Results.TotalResults 0) }}
|
||||
<p>No results found for your query: <strong>{{ .SearchKey }}</strong>.</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<ul class="search-results">
|
||||
{{ range .Results.Articles }}
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>News App Demo</title>
|
||||
<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="{{ .Query }}"
|
||||
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 .Query "") and (eq .Results.TotalResults 0) }}
|
||||
<p>
|
||||
No results found for your query: <strong>{{ .Query }}</strong
|
||||
>.
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
<ul class="search-results">
|
||||
<!-- prettier-ignore -->
|
||||
{{ range.Results.Articles }}
|
||||
<li class="news-article">
|
||||
<div>
|
||||
<a target="_blank" rel="noreferrer noopener" href="{{.URL}}">
|
||||
@@ -39,20 +57,28 @@
|
||||
<time class="published-date">{{ .FormatPublishedDate }}</time>
|
||||
</div>
|
||||
</div>
|
||||
<img class="article-image" src="{{ .URLToImage }}">
|
||||
<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>
|
||||
<!-- prettier-ignore -->
|
||||
{{ end }}
|
||||
</ul>
|
||||
<div class="pagination">
|
||||
{{ if (gt .NextPage 2) }}
|
||||
<a
|
||||
href="/search?q={{ .Query }}&page={{ .PreviousPage }}"
|
||||
class="button previous-page"
|
||||
>Previous</a
|
||||
>
|
||||
{{ end }}
|
||||
{{ if (ne .IsLastPage true) }}
|
||||
<a
|
||||
href="/search?q={{ .Query }}&page={{ .NextPage }}"
|
||||
class="button next-page"
|
||||
>Next</a
|
||||
>
|
||||
{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
174
main.go
174
main.go
@@ -1,9 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"bytes"
|
||||
"html/template"
|
||||
"log"
|
||||
"math"
|
||||
@@ -12,53 +10,24 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/freshman-tech/news-demo/news"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
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
|
||||
Query string
|
||||
NextPage int
|
||||
TotalPages int
|
||||
Results Results
|
||||
Results *news.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
|
||||
@@ -67,101 +36,94 @@ func (s *Search) CurrentPage() int {
|
||||
return s.NextPage - 1
|
||||
}
|
||||
|
||||
type NewsAPIError struct {
|
||||
Status string `json:"status"`
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
func (s *Search) PreviousPage() int {
|
||||
return s.CurrentPage() - 1
|
||||
}
|
||||
|
||||
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
tpl.Execute(w, nil)
|
||||
buf := &bytes.Buffer{}
|
||||
err := tpl.Execute(buf, nil)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
buf.WriteTo(w)
|
||||
}
|
||||
|
||||
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 {
|
||||
newError := &NewsAPIError{}
|
||||
err := json.NewDecoder(resp.Body).Decode(newError)
|
||||
|
||||
func searchHandler(newsapi *news.Client) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
u, err := url.Parse(r.URL.String())
|
||||
if err != nil {
|
||||
http.Error(w, "Unexpected server error", http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, newError.Message, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
params := u.Query()
|
||||
searchQuery := params.Get("q")
|
||||
page := params.Get("page")
|
||||
if page == "" {
|
||||
page = "1"
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&search.Results)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
results, err := newsapi.FetchEverything(searchQuery, page)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
search.TotalPages = int(math.Ceil(float64(search.Results.TotalResults / pageSize)))
|
||||
if ok := !search.IsLastPage(); ok {
|
||||
search.NextPage++
|
||||
}
|
||||
nextPage, err := strconv.Atoi(page)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = tpl.Execute(w, search)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
search := &Search{
|
||||
Query: searchQuery,
|
||||
NextPage: nextPage,
|
||||
TotalPages: int(math.Ceil(float64(results.TotalResults / newsapi.PageSize))),
|
||||
Results: results,
|
||||
}
|
||||
|
||||
if ok := !search.IsLastPage(); ok {
|
||||
search.NextPage++
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
err = tpl.Execute(buf, search)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
buf.WriteTo(w)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Println("Error loading .env file")
|
||||
}
|
||||
|
||||
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")
|
||||
apiKey := os.Getenv("NEWS_API_KEY")
|
||||
if apiKey == "" {
|
||||
log.Fatal("Env: apiKey must be set")
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
myClient := &http.Client{Timeout: 10 * time.Second}
|
||||
newsapi := news.NewClient(myClient, apiKey, 20)
|
||||
|
||||
fs := http.FileServer(http.Dir("assets"))
|
||||
mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
|
||||
|
||||
mux.HandleFunc("/search", searchHandler)
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
|
||||
mux.HandleFunc("/search", searchHandler(newsapi))
|
||||
mux.HandleFunc("/", indexHandler)
|
||||
http.ListenAndServe(":"+port, mux)
|
||||
}
|
||||
|
||||
71
news/news.go
Normal file
71
news/news.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package news
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Article struct {
|
||||
Source struct {
|
||||
ID interface{} `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `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 Client struct {
|
||||
http *http.Client
|
||||
key string
|
||||
PageSize int
|
||||
}
|
||||
|
||||
func (c *Client) FetchEverything(query, page string) (*Results, error) {
|
||||
endpoint := fmt.Sprintf("https://newsapi.org/v2/everything?q=%s&pageSize=%d&page=%s&apiKey=%s&sortBy=publishedAt&language=en", url.QueryEscape(query), c.PageSize, page, c.key)
|
||||
resp, err := c.http.Get(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf(string(body))
|
||||
}
|
||||
|
||||
res := &Results{}
|
||||
return res, json.Unmarshal(body, res)
|
||||
}
|
||||
|
||||
func NewClient(httpClient *http.Client, key string, pageSize int) *Client {
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
|
||||
return &Client{httpClient, key, pageSize}
|
||||
}
|
||||
Reference in New Issue
Block a user