Add checkpoints

This commit is contained in:
Ayooluwa Isaiah
2020-11-22 20:47:20 +01:00
parent fa04918530
commit cc4a2af266
29 changed files with 1769 additions and 0 deletions

View File

@@ -0,0 +1,173 @@
html {
box-sizing: border-box;
}
*, *::before, *::after {
box-sizing: inherit;
margin: 0;
padding: 0;
}
:root {
--light-green: #00ff00;
--dark-green: #003b00;
--dark-grey: #777;
--light-grey: #dadce0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
}
a {
text-decoration: none;
color: #333;
}
a:hover {
text-decoration: underline;
}
a.button {
border: 2px solid #004400;
color: var(--dark-green);
border-radius: 4px;
padding: 6px 24px;
font-size: 14px;
font-weight: 400;
}
a.button:hover {
text-decoration: none;
background-color: var(--dark-green);
color: var(--light-green);
}
header {
width: 100%;
height: 50px;
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
background-color: var(--light-green);
padding: 5px 10px;
align-items: center;
}
.logo {
color: #002200;
}
form {
height: calc(100% - 10px);
}
.search-input {
width: 500px;
height: 100%;
border-radius: 4px;
border-color: transparent;
background-color: var(--dark-green);
color: var(--light-green);
font-size: 16px;
line-height: 1.4;
padding-left: 5px;
}
.container {
width: 100%;
max-width: 720px;
margin: 0 auto;
padding: 80px 20px 40px;
}
.result-count {
color: var(--dark-grey);
text-align: center;
margin-bottom: 15px;
}
.search-results {
list-style: none;
}
.news-article {
display: flex;
align-items: flex-start;
margin-bottom: 30px;
border: 1px solid var(--light-grey);
padding: 15px;
border-radius: 4px;
justify-content: space-between;
}
.article-image {
width: 200px;
flex-grow: 0;
flex-shrink: 0;
margin-left: 20px;
}
.title {
margin-bottom: 15px;
}
.description {
color: var(--dark-grey);
margin-bottom: 15px;
}
.metadata {
display: flex;
color: var(--dark-green);
font-size: 14px;
}
.published-date::before {
content: '\0000a0\002022\0000a0';
margin: 0 3px;
}
.pagination {
margin-top: 20px;
}
.previous-page {
margin-right: 20px;
}
@media screen and (max-width: 550px) {
header {
flex-direction: column;
height: auto;
padding-bottom: 10px;
}
.logo {
display: inline-block;
margin-bottom: 10px;
}
form, .search-input {
width: 100%;
}
.github-button {
display: none;
}
.title {
font-size: 18px;
}
.description {
font-size: 14px;
}
.article-image {
display: none;
}
}

5
checkpoints/01/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/freshman-tech/news-demo-starter-files
go 1.15
require github.com/joho/godotenv v1.3.0

2
checkpoints/01/go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=

32
checkpoints/01/index.html Normal file
View File

@@ -0,0 +1,32 @@
<!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=""
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>
</main>
</body>
</html>

35
checkpoints/01/main.go Normal file
View File

@@ -0,0 +1,35 @@
package main
import (
"html/template"
"log"
"net/http"
"os"
"github.com/joho/godotenv"
)
var tpl = template.Must(template.ParseFiles("index.html"))
func indexHandler(w http.ResponseWriter, r *http.Request) {
tpl.Execute(w, nil)
}
func main() {
err := godotenv.Load()
if err != nil {
log.Println("Error loading .env file")
}
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
fs := http.FileServer(http.Dir("assets"))
mux := http.NewServeMux()
mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
mux.HandleFunc("/", indexHandler)
http.ListenAndServe(":"+port, mux)
}

View File

@@ -0,0 +1,173 @@
html {
box-sizing: border-box;
}
*, *::before, *::after {
box-sizing: inherit;
margin: 0;
padding: 0;
}
:root {
--light-green: #00ff00;
--dark-green: #003b00;
--dark-grey: #777;
--light-grey: #dadce0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
}
a {
text-decoration: none;
color: #333;
}
a:hover {
text-decoration: underline;
}
a.button {
border: 2px solid #004400;
color: var(--dark-green);
border-radius: 4px;
padding: 6px 24px;
font-size: 14px;
font-weight: 400;
}
a.button:hover {
text-decoration: none;
background-color: var(--dark-green);
color: var(--light-green);
}
header {
width: 100%;
height: 50px;
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
background-color: var(--light-green);
padding: 5px 10px;
align-items: center;
}
.logo {
color: #002200;
}
form {
height: calc(100% - 10px);
}
.search-input {
width: 500px;
height: 100%;
border-radius: 4px;
border-color: transparent;
background-color: var(--dark-green);
color: var(--light-green);
font-size: 16px;
line-height: 1.4;
padding-left: 5px;
}
.container {
width: 100%;
max-width: 720px;
margin: 0 auto;
padding: 80px 20px 40px;
}
.result-count {
color: var(--dark-grey);
text-align: center;
margin-bottom: 15px;
}
.search-results {
list-style: none;
}
.news-article {
display: flex;
align-items: flex-start;
margin-bottom: 30px;
border: 1px solid var(--light-grey);
padding: 15px;
border-radius: 4px;
justify-content: space-between;
}
.article-image {
width: 200px;
flex-grow: 0;
flex-shrink: 0;
margin-left: 20px;
}
.title {
margin-bottom: 15px;
}
.description {
color: var(--dark-grey);
margin-bottom: 15px;
}
.metadata {
display: flex;
color: var(--dark-green);
font-size: 14px;
}
.published-date::before {
content: '\0000a0\002022\0000a0';
margin: 0 3px;
}
.pagination {
margin-top: 20px;
}
.previous-page {
margin-right: 20px;
}
@media screen and (max-width: 550px) {
header {
flex-direction: column;
height: auto;
padding-bottom: 10px;
}
.logo {
display: inline-block;
margin-bottom: 10px;
}
form, .search-input {
width: 100%;
}
.github-button {
display: none;
}
.title {
font-size: 18px;
}
.description {
font-size: 14px;
}
.article-image {
display: none;
}
}

5
checkpoints/02/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/freshman-tech/news-demo-starter-files
go 1.15
require github.com/joho/godotenv v1.3.0

2
checkpoints/02/go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=

32
checkpoints/02/index.html Normal file
View File

@@ -0,0 +1,32 @@
<!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=""
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>
</main>
</body>
</html>

68
checkpoints/02/main.go Normal file
View File

@@ -0,0 +1,68 @@
package main
import (
"fmt"
"html/template"
"log"
"net/http"
"net/url"
"os"
"time"
"github.com/freshman-tech/news-demo-starter-files/news"
"github.com/joho/godotenv"
)
var tpl = template.Must(template.ParseFiles("index.html"))
func indexHandler(w http.ResponseWriter, r *http.Request) {
tpl.Execute(w, nil)
}
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, err.Error(), http.StatusInternalServerError)
return
}
params := u.Query()
searchQuery := params.Get("q")
page := params.Get("page")
if page == "" {
page = "1"
}
fmt.Println("Search Query is: ", searchQuery)
fmt.Println("Page is: ", page)
}
}
func main() {
err := godotenv.Load()
if err != nil {
log.Println("Error loading .env file")
}
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
apiKey := os.Getenv("NEWS_API_KEY")
if apiKey == "" {
log.Fatal("Env: apiKey must be set")
}
myClient := &http.Client{Timeout: 10 * time.Second}
newsapi := news.NewClient(myClient, apiKey, 20)
fs := http.FileServer(http.Dir("assets"))
mux := http.NewServeMux()
mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
mux.HandleFunc("/search", searchHandler(newsapi))
mux.HandleFunc("/", indexHandler)
http.ListenAndServe(":"+port, mux)
}

View File

@@ -0,0 +1,17 @@
package news
import "net/http"
type Client struct {
http *http.Client
key string
PageSize int
}
func NewClient(httpClient *http.Client, key string, pageSize int) *Client {
if pageSize > 100 {
pageSize = 100
}
return &Client{httpClient, key, pageSize}
}

View File

@@ -0,0 +1,173 @@
html {
box-sizing: border-box;
}
*, *::before, *::after {
box-sizing: inherit;
margin: 0;
padding: 0;
}
:root {
--light-green: #00ff00;
--dark-green: #003b00;
--dark-grey: #777;
--light-grey: #dadce0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
}
a {
text-decoration: none;
color: #333;
}
a:hover {
text-decoration: underline;
}
a.button {
border: 2px solid #004400;
color: var(--dark-green);
border-radius: 4px;
padding: 6px 24px;
font-size: 14px;
font-weight: 400;
}
a.button:hover {
text-decoration: none;
background-color: var(--dark-green);
color: var(--light-green);
}
header {
width: 100%;
height: 50px;
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
background-color: var(--light-green);
padding: 5px 10px;
align-items: center;
}
.logo {
color: #002200;
}
form {
height: calc(100% - 10px);
}
.search-input {
width: 500px;
height: 100%;
border-radius: 4px;
border-color: transparent;
background-color: var(--dark-green);
color: var(--light-green);
font-size: 16px;
line-height: 1.4;
padding-left: 5px;
}
.container {
width: 100%;
max-width: 720px;
margin: 0 auto;
padding: 80px 20px 40px;
}
.result-count {
color: var(--dark-grey);
text-align: center;
margin-bottom: 15px;
}
.search-results {
list-style: none;
}
.news-article {
display: flex;
align-items: flex-start;
margin-bottom: 30px;
border: 1px solid var(--light-grey);
padding: 15px;
border-radius: 4px;
justify-content: space-between;
}
.article-image {
width: 200px;
flex-grow: 0;
flex-shrink: 0;
margin-left: 20px;
}
.title {
margin-bottom: 15px;
}
.description {
color: var(--dark-grey);
margin-bottom: 15px;
}
.metadata {
display: flex;
color: var(--dark-green);
font-size: 14px;
}
.published-date::before {
content: '\0000a0\002022\0000a0';
margin: 0 3px;
}
.pagination {
margin-top: 20px;
}
.previous-page {
margin-right: 20px;
}
@media screen and (max-width: 550px) {
header {
flex-direction: column;
height: auto;
padding-bottom: 10px;
}
.logo {
display: inline-block;
margin-bottom: 10px;
}
form, .search-input {
width: 100%;
}
.github-button {
display: none;
}
.title {
font-size: 18px;
}
.description {
font-size: 14px;
}
.article-image {
display: none;
}
}

5
checkpoints/03/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/freshman-tech/news-demo-starter-files
go 1.15
require github.com/joho/godotenv v1.3.0

2
checkpoints/03/go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=

32
checkpoints/03/index.html Normal file
View File

@@ -0,0 +1,32 @@
<!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=""
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>
</main>
</body>
</html>

73
checkpoints/03/main.go Normal file
View File

@@ -0,0 +1,73 @@
package main
import (
"fmt"
"html/template"
"log"
"net/http"
"net/url"
"os"
"time"
"github.com/freshman-tech/news-demo-starter-files/news"
"github.com/joho/godotenv"
)
var tpl = template.Must(template.ParseFiles("index.html"))
func indexHandler(w http.ResponseWriter, r *http.Request) {
tpl.Execute(w, nil)
}
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, err.Error(), http.StatusInternalServerError)
return
}
params := u.Query()
searchQuery := params.Get("q")
page := params.Get("page")
if page == "" {
page = "1"
}
results, err := newsapi.FetchEverything(searchQuery, page)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Printf("%+v", results)
}
}
func main() {
err := godotenv.Load()
if err != nil {
log.Println("Error loading .env file")
}
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
apiKey := os.Getenv("NEWS_API_KEY")
if apiKey == "" {
log.Fatal("Env: apiKey must be set")
}
myClient := &http.Client{Timeout: 10 * time.Second}
newsapi := news.NewClient(myClient, apiKey, 20)
fs := http.FileServer(http.Dir("assets"))
mux := http.NewServeMux()
mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
mux.HandleFunc("/search", searchHandler(newsapi))
mux.HandleFunc("/", indexHandler)
http.ListenAndServe(":"+port, mux)
}

View File

@@ -0,0 +1,66 @@
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"`
}
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}
}

View File

@@ -0,0 +1,173 @@
html {
box-sizing: border-box;
}
*, *::before, *::after {
box-sizing: inherit;
margin: 0;
padding: 0;
}
:root {
--light-green: #00ff00;
--dark-green: #003b00;
--dark-grey: #777;
--light-grey: #dadce0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
}
a {
text-decoration: none;
color: #333;
}
a:hover {
text-decoration: underline;
}
a.button {
border: 2px solid #004400;
color: var(--dark-green);
border-radius: 4px;
padding: 6px 24px;
font-size: 14px;
font-weight: 400;
}
a.button:hover {
text-decoration: none;
background-color: var(--dark-green);
color: var(--light-green);
}
header {
width: 100%;
height: 50px;
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
background-color: var(--light-green);
padding: 5px 10px;
align-items: center;
}
.logo {
color: #002200;
}
form {
height: calc(100% - 10px);
}
.search-input {
width: 500px;
height: 100%;
border-radius: 4px;
border-color: transparent;
background-color: var(--dark-green);
color: var(--light-green);
font-size: 16px;
line-height: 1.4;
padding-left: 5px;
}
.container {
width: 100%;
max-width: 720px;
margin: 0 auto;
padding: 80px 20px 40px;
}
.result-count {
color: var(--dark-grey);
text-align: center;
margin-bottom: 15px;
}
.search-results {
list-style: none;
}
.news-article {
display: flex;
align-items: flex-start;
margin-bottom: 30px;
border: 1px solid var(--light-grey);
padding: 15px;
border-radius: 4px;
justify-content: space-between;
}
.article-image {
width: 200px;
flex-grow: 0;
flex-shrink: 0;
margin-left: 20px;
}
.title {
margin-bottom: 15px;
}
.description {
color: var(--dark-grey);
margin-bottom: 15px;
}
.metadata {
display: flex;
color: var(--dark-green);
font-size: 14px;
}
.published-date::before {
content: '\0000a0\002022\0000a0';
margin: 0 3px;
}
.pagination {
margin-top: 20px;
}
.previous-page {
margin-right: 20px;
}
@media screen and (max-width: 550px) {
header {
flex-direction: column;
height: auto;
padding-bottom: 10px;
}
.logo {
display: inline-block;
margin-bottom: 10px;
}
form, .search-input {
width: 100%;
}
.github-button {
display: none;
}
.title {
font-size: 18px;
}
.description {
font-size: 14px;
}
.article-image {
display: none;
}
}

5
checkpoints/04/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/freshman-tech/news-demo-starter-files
go 1.15
require github.com/joho/godotenv v1.3.0

2
checkpoints/04/go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=

51
checkpoints/04/index.html Normal file
View File

@@ -0,0 +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="{{ .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">
<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">{{ .PublishedAt }}</time>
</div>
</div>
<img class="article-image" src="{{ .URLToImage }}" />
</li>
{{ end }}
</ul>
</section>
</main>
</body>
</html>

109
checkpoints/04/main.go Normal file
View File

@@ -0,0 +1,109 @@
package main
import (
"bytes"
"html/template"
"log"
"math"
"net/http"
"net/url"
"os"
"strconv"
"time"
"github.com/freshman-tech/news-demo-starter-files/news"
"github.com/joho/godotenv"
)
var tpl = template.Must(template.ParseFiles("index.html"))
type Search struct {
Query string
NextPage int
TotalPages int
Results *news.Results
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
buf := &bytes.Buffer{}
err := tpl.Execute(buf, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
buf.WriteTo(w)
}
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, err.Error(), http.StatusInternalServerError)
return
}
params := u.Query()
searchQuery := params.Get("q")
page := params.Get("page")
if page == "" {
page = "1"
}
results, err := newsapi.FetchEverything(searchQuery, page)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
nextPage, err := strconv.Atoi(page)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
search := &Search{
Query: searchQuery,
NextPage: nextPage,
TotalPages: int(math.Ceil(float64(results.TotalResults / newsapi.PageSize))),
Results: results,
}
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 := os.Getenv("NEWS_API_KEY")
if apiKey == "" {
log.Fatal("Env: apiKey must be set")
}
myClient := &http.Client{Timeout: 10 * time.Second}
newsapi := news.NewClient(myClient, apiKey, 20)
fs := http.FileServer(http.Dir("assets"))
mux := http.NewServeMux()
mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
mux.HandleFunc("/search", searchHandler(newsapi))
mux.HandleFunc("/", indexHandler)
http.ListenAndServe(":"+port, mux)
}

View File

@@ -0,0 +1,66 @@
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"`
}
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}
}

View File

@@ -0,0 +1,173 @@
html {
box-sizing: border-box;
}
*, *::before, *::after {
box-sizing: inherit;
margin: 0;
padding: 0;
}
:root {
--light-green: #00ff00;
--dark-green: #003b00;
--dark-grey: #777;
--light-grey: #dadce0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
}
a {
text-decoration: none;
color: #333;
}
a:hover {
text-decoration: underline;
}
a.button {
border: 2px solid #004400;
color: var(--dark-green);
border-radius: 4px;
padding: 6px 24px;
font-size: 14px;
font-weight: 400;
}
a.button:hover {
text-decoration: none;
background-color: var(--dark-green);
color: var(--light-green);
}
header {
width: 100%;
height: 50px;
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
background-color: var(--light-green);
padding: 5px 10px;
align-items: center;
}
.logo {
color: #002200;
}
form {
height: calc(100% - 10px);
}
.search-input {
width: 500px;
height: 100%;
border-radius: 4px;
border-color: transparent;
background-color: var(--dark-green);
color: var(--light-green);
font-size: 16px;
line-height: 1.4;
padding-left: 5px;
}
.container {
width: 100%;
max-width: 720px;
margin: 0 auto;
padding: 80px 20px 40px;
}
.result-count {
color: var(--dark-grey);
text-align: center;
margin-bottom: 15px;
}
.search-results {
list-style: none;
}
.news-article {
display: flex;
align-items: flex-start;
margin-bottom: 30px;
border: 1px solid var(--light-grey);
padding: 15px;
border-radius: 4px;
justify-content: space-between;
}
.article-image {
width: 200px;
flex-grow: 0;
flex-shrink: 0;
margin-left: 20px;
}
.title {
margin-bottom: 15px;
}
.description {
color: var(--dark-grey);
margin-bottom: 15px;
}
.metadata {
display: flex;
color: var(--dark-green);
font-size: 14px;
}
.published-date::before {
content: '\0000a0\002022\0000a0';
margin: 0 3px;
}
.pagination {
margin-top: 20px;
}
.previous-page {
margin-right: 20px;
}
@media screen and (max-width: 550px) {
header {
flex-direction: column;
height: auto;
padding-bottom: 10px;
}
.logo {
display: inline-block;
margin-bottom: 10px;
}
form, .search-input {
width: 100%;
}
.github-button {
display: none;
}
.title {
font-size: 18px;
}
.description {
font-size: 14px;
}
.article-image {
display: none;
}
}

5
checkpoints/05/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/freshman-tech/news-demo-starter-files
go 1.15
require github.com/joho/godotenv v1.3.0

2
checkpoints/05/go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=

88
checkpoints/05/index.html Normal file
View File

@@ -0,0 +1,88 @@
<!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="{{ .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 .Results }}
{{ 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 .Query "") (eq .Results.TotalResults 0) }}
<p>
No results found for your query: <strong>{{ .Query }}</strong
>.
</p>
{{ end }}
{{ end }}
</div>
<ul class="search-results">
<!-- prettier-ignore -->
{{ 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>
<!-- prettier-ignore -->
{{ end }}
</ul>
<div class="pagination">
{{ if . }}
{{ 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 }}
{{ end }}
</div>
</section>
</main>
</body>
</html>

129
checkpoints/05/main.go Normal file
View File

@@ -0,0 +1,129 @@
package main
import (
"bytes"
"html/template"
"log"
"math"
"net/http"
"net/url"
"os"
"strconv"
"time"
"github.com/freshman-tech/news-demo-starter-files/news"
"github.com/joho/godotenv"
)
var tpl = template.Must(template.ParseFiles("index.html"))
type Search struct {
Query string
NextPage int
TotalPages int
Results *news.Results
}
func (s *Search) IsLastPage() bool {
return s.NextPage >= s.TotalPages
}
func (s *Search) CurrentPage() int {
if s.NextPage == 1 {
return s.NextPage
}
return s.NextPage - 1
}
func (s *Search) PreviousPage() int {
return s.CurrentPage() - 1
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
buf := &bytes.Buffer{}
err := tpl.Execute(buf, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
buf.WriteTo(w)
}
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, err.Error(), http.StatusInternalServerError)
return
}
params := u.Query()
searchQuery := params.Get("q")
page := params.Get("page")
if page == "" {
page = "1"
}
results, err := newsapi.FetchEverything(searchQuery, page)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
nextPage, err := strconv.Atoi(page)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
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 := os.Getenv("NEWS_API_KEY")
if apiKey == "" {
log.Fatal("Env: apiKey must be set")
}
myClient := &http.Client{Timeout: 10 * time.Second}
newsapi := news.NewClient(myClient, apiKey, 20)
fs := http.FileServer(http.Dir("assets"))
mux := http.NewServeMux()
mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
mux.HandleFunc("/search", searchHandler(newsapi))
mux.HandleFunc("/", indexHandler)
http.ListenAndServe(":"+port, mux)
}

View 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}
}