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 + + + +
+
+ +
+ +
+ View on GitHub +
+
+ + 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 + + + +
+
+ +
+ +
+ View on GitHub +
+
+ + 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 + + + +
+
+ +
+ +
+ View on GitHub +
+
+ + 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 + + + +
+
+ +
+ +
+ View on GitHub +
+
+ +
+
+ + 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 + + + +
+
+ +
+ +
+ View on GitHub +
+
+
+ {{ 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 }} +
+ + +
+
+ + 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} +}