Add checkpoints
This commit is contained in:
173
checkpoints/01/assets/style.css
Normal file
173
checkpoints/01/assets/style.css
Normal 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
5
checkpoints/01/go.mod
Normal 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
2
checkpoints/01/go.sum
Normal 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
32
checkpoints/01/index.html
Normal 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
35
checkpoints/01/main.go
Normal 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)
|
||||||
|
}
|
||||||
173
checkpoints/02/assets/style.css
Normal file
173
checkpoints/02/assets/style.css
Normal 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
5
checkpoints/02/go.mod
Normal 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
2
checkpoints/02/go.sum
Normal 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
32
checkpoints/02/index.html
Normal 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
68
checkpoints/02/main.go
Normal 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)
|
||||||
|
}
|
||||||
17
checkpoints/02/news/news.go
Normal file
17
checkpoints/02/news/news.go
Normal 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}
|
||||||
|
}
|
||||||
173
checkpoints/03/assets/style.css
Normal file
173
checkpoints/03/assets/style.css
Normal 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
5
checkpoints/03/go.mod
Normal 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
2
checkpoints/03/go.sum
Normal 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
32
checkpoints/03/index.html
Normal 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
73
checkpoints/03/main.go
Normal 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)
|
||||||
|
}
|
||||||
66
checkpoints/03/news/news.go
Normal file
66
checkpoints/03/news/news.go
Normal 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}
|
||||||
|
}
|
||||||
173
checkpoints/04/assets/style.css
Normal file
173
checkpoints/04/assets/style.css
Normal 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
5
checkpoints/04/go.mod
Normal 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
2
checkpoints/04/go.sum
Normal 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
51
checkpoints/04/index.html
Normal 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
109
checkpoints/04/main.go
Normal 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)
|
||||||
|
}
|
||||||
66
checkpoints/04/news/news.go
Normal file
66
checkpoints/04/news/news.go
Normal 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}
|
||||||
|
}
|
||||||
173
checkpoints/05/assets/style.css
Normal file
173
checkpoints/05/assets/style.css
Normal 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
5
checkpoints/05/go.mod
Normal 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
2
checkpoints/05/go.sum
Normal 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
88
checkpoints/05/index.html
Normal 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
129
checkpoints/05/main.go
Normal 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)
|
||||||
|
}
|
||||||
71
checkpoints/05/news/news.go
Normal file
71
checkpoints/05/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