diff --git a/checkpoints/01/assets/style.css b/checkpoints/01/assets/style.css
new file mode 100644
index 0000000..0b1a130
--- /dev/null
+++ b/checkpoints/01/assets/style.css
@@ -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;
+ }
+}
diff --git a/checkpoints/01/go.mod b/checkpoints/01/go.mod
new file mode 100644
index 0000000..d79cf4b
--- /dev/null
+++ b/checkpoints/01/go.mod
@@ -0,0 +1,5 @@
+module github.com/freshman-tech/news-demo-starter-files
+
+go 1.15
+
+require github.com/joho/godotenv v1.3.0
diff --git a/checkpoints/01/go.sum b/checkpoints/01/go.sum
new file mode 100644
index 0000000..ead7071
--- /dev/null
+++ b/checkpoints/01/go.sum
@@ -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=
diff --git a/checkpoints/01/index.html b/checkpoints/01/index.html
new file mode 100644
index 0000000..fa4ac18
--- /dev/null
+++ b/checkpoints/01/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ News App Demo
+
+
+
+
+
+
+
+
diff --git a/checkpoints/01/main.go b/checkpoints/01/main.go
new file mode 100644
index 0000000..3f431bb
--- /dev/null
+++ b/checkpoints/01/main.go
@@ -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)
+}
diff --git a/checkpoints/02/assets/style.css b/checkpoints/02/assets/style.css
new file mode 100644
index 0000000..0b1a130
--- /dev/null
+++ b/checkpoints/02/assets/style.css
@@ -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;
+ }
+}
diff --git a/checkpoints/02/go.mod b/checkpoints/02/go.mod
new file mode 100644
index 0000000..d79cf4b
--- /dev/null
+++ b/checkpoints/02/go.mod
@@ -0,0 +1,5 @@
+module github.com/freshman-tech/news-demo-starter-files
+
+go 1.15
+
+require github.com/joho/godotenv v1.3.0
diff --git a/checkpoints/02/go.sum b/checkpoints/02/go.sum
new file mode 100644
index 0000000..ead7071
--- /dev/null
+++ b/checkpoints/02/go.sum
@@ -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=
diff --git a/checkpoints/02/index.html b/checkpoints/02/index.html
new file mode 100644
index 0000000..fa4ac18
--- /dev/null
+++ b/checkpoints/02/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ News App Demo
+
+
+
+
+
+
+
+
diff --git a/checkpoints/02/main.go b/checkpoints/02/main.go
new file mode 100644
index 0000000..97b5ad2
--- /dev/null
+++ b/checkpoints/02/main.go
@@ -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)
+}
diff --git a/checkpoints/02/news/news.go b/checkpoints/02/news/news.go
new file mode 100644
index 0000000..7d97651
--- /dev/null
+++ b/checkpoints/02/news/news.go
@@ -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}
+}
diff --git a/checkpoints/03/assets/style.css b/checkpoints/03/assets/style.css
new file mode 100644
index 0000000..0b1a130
--- /dev/null
+++ b/checkpoints/03/assets/style.css
@@ -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;
+ }
+}
diff --git a/checkpoints/03/go.mod b/checkpoints/03/go.mod
new file mode 100644
index 0000000..d79cf4b
--- /dev/null
+++ b/checkpoints/03/go.mod
@@ -0,0 +1,5 @@
+module github.com/freshman-tech/news-demo-starter-files
+
+go 1.15
+
+require github.com/joho/godotenv v1.3.0
diff --git a/checkpoints/03/go.sum b/checkpoints/03/go.sum
new file mode 100644
index 0000000..ead7071
--- /dev/null
+++ b/checkpoints/03/go.sum
@@ -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=
diff --git a/checkpoints/03/index.html b/checkpoints/03/index.html
new file mode 100644
index 0000000..fa4ac18
--- /dev/null
+++ b/checkpoints/03/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ News App Demo
+
+
+
+
+
+
+
+
diff --git a/checkpoints/03/main.go b/checkpoints/03/main.go
new file mode 100644
index 0000000..fd8c79e
--- /dev/null
+++ b/checkpoints/03/main.go
@@ -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)
+}
diff --git a/checkpoints/03/news/news.go b/checkpoints/03/news/news.go
new file mode 100644
index 0000000..07071e8
--- /dev/null
+++ b/checkpoints/03/news/news.go
@@ -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}
+}
diff --git a/checkpoints/04/assets/style.css b/checkpoints/04/assets/style.css
new file mode 100644
index 0000000..0b1a130
--- /dev/null
+++ b/checkpoints/04/assets/style.css
@@ -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;
+ }
+}
diff --git a/checkpoints/04/go.mod b/checkpoints/04/go.mod
new file mode 100644
index 0000000..d79cf4b
--- /dev/null
+++ b/checkpoints/04/go.mod
@@ -0,0 +1,5 @@
+module github.com/freshman-tech/news-demo-starter-files
+
+go 1.15
+
+require github.com/joho/godotenv v1.3.0
diff --git a/checkpoints/04/go.sum b/checkpoints/04/go.sum
new file mode 100644
index 0000000..ead7071
--- /dev/null
+++ b/checkpoints/04/go.sum
@@ -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=
diff --git a/checkpoints/04/index.html b/checkpoints/04/index.html
new file mode 100644
index 0000000..efe3749
--- /dev/null
+++ b/checkpoints/04/index.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+ News App Demo
+
+
+
+
+
+
+
+ {{ range.Results.Articles }}
+ -
+
+
+
+ {{ end }}
+
+
+
+
+
diff --git a/checkpoints/04/main.go b/checkpoints/04/main.go
new file mode 100644
index 0000000..7da6fa5
--- /dev/null
+++ b/checkpoints/04/main.go
@@ -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)
+}
diff --git a/checkpoints/04/news/news.go b/checkpoints/04/news/news.go
new file mode 100644
index 0000000..07071e8
--- /dev/null
+++ b/checkpoints/04/news/news.go
@@ -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}
+}
diff --git a/checkpoints/05/assets/style.css b/checkpoints/05/assets/style.css
new file mode 100644
index 0000000..0b1a130
--- /dev/null
+++ b/checkpoints/05/assets/style.css
@@ -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;
+ }
+}
diff --git a/checkpoints/05/go.mod b/checkpoints/05/go.mod
new file mode 100644
index 0000000..d79cf4b
--- /dev/null
+++ b/checkpoints/05/go.mod
@@ -0,0 +1,5 @@
+module github.com/freshman-tech/news-demo-starter-files
+
+go 1.15
+
+require github.com/joho/godotenv v1.3.0
diff --git a/checkpoints/05/go.sum b/checkpoints/05/go.sum
new file mode 100644
index 0000000..ead7071
--- /dev/null
+++ b/checkpoints/05/go.sum
@@ -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=
diff --git a/checkpoints/05/index.html b/checkpoints/05/index.html
new file mode 100644
index 0000000..7e9aaf8
--- /dev/null
+++ b/checkpoints/05/index.html
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+ News App Demo
+
+
+
+
+
+
+
+ {{ if .Results }}
+ {{ if (gt .Results.TotalResults 0)}}
+
+ About {{ .Results.TotalResults }} results were
+ found. You are on page {{ .CurrentPage }} of
+ {{ .TotalPages }}.
+
+ {{ else if and (ne .Query "") (eq .Results.TotalResults 0) }}
+
+ No results found for your query: {{ .Query }}.
+
+ {{ end }}
+ {{ end }}
+
+
+
+ {{ range.Results.Articles }}
+ -
+
+
+
+
+ {{ end }}
+
+
+
+
+
+
diff --git a/checkpoints/05/main.go b/checkpoints/05/main.go
new file mode 100644
index 0000000..0e30011
--- /dev/null
+++ b/checkpoints/05/main.go
@@ -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)
+}
diff --git a/checkpoints/05/news/news.go b/checkpoints/05/news/news.go
new file mode 100644
index 0000000..6025393
--- /dev/null
+++ b/checkpoints/05/news/news.go
@@ -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}
+}