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