├── Procfile
├── .env.example
├── go.mod
├── checkpoints
├── 01
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ ├── index.html
│ └── assets
│ │ └── style.css
├── 02
│ ├── go.mod
│ ├── go.sum
│ ├── news
│ │ └── news.go
│ ├── index.html
│ ├── main.go
│ └── assets
│ │ └── style.css
├── 03
│ ├── go.mod
│ ├── go.sum
│ ├── index.html
│ ├── news
│ │ └── news.go
│ ├── main.go
│ └── assets
│ │ └── style.css
├── 04
│ ├── go.mod
│ ├── go.sum
│ ├── news
│ │ └── news.go
│ ├── index.html
│ ├── main.go
│ └── assets
│ │ └── style.css
└── 05
│ ├── go.mod
│ ├── go.sum
│ ├── news
│ └── news.go
│ ├── index.html
│ ├── main.go
│ └── assets
│ └── style.css
├── go.sum
├── LICENCE
├── README.md
├── news
└── news.go
├── index.html
├── main.go
└── assets
└── style.css
/Procfile:
--------------------------------------------------------------------------------
1 | web: bin/news-demo
2 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | PORT=3000
2 | NEWS_API_KEY=
3 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/freshman-tech/news-demo
2 |
3 | go 1.15
4 |
5 | require github.com/joho/godotenv v1.3.0
6 |
--------------------------------------------------------------------------------
/checkpoints/01/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/freshman-tech/news-demo-starter-files
2 |
3 | go 1.15
4 |
5 | require github.com/joho/godotenv v1.3.0
6 |
--------------------------------------------------------------------------------
/checkpoints/02/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/freshman-tech/news-demo-starter-files
2 |
3 | go 1.15
4 |
5 | require github.com/joho/godotenv v1.3.0
6 |
--------------------------------------------------------------------------------
/checkpoints/03/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/freshman-tech/news-demo-starter-files
2 |
3 | go 1.15
4 |
5 | require github.com/joho/godotenv v1.3.0
6 |
--------------------------------------------------------------------------------
/checkpoints/04/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/freshman-tech/news-demo-starter-files
2 |
3 | go 1.15
4 |
5 | require github.com/joho/godotenv v1.3.0
6 |
--------------------------------------------------------------------------------
/checkpoints/05/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/freshman-tech/news-demo-starter-files
2 |
3 | go 1.15
4 |
5 | require github.com/joho/godotenv v1.3.0
6 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
2 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
3 |
--------------------------------------------------------------------------------
/checkpoints/01/go.sum:
--------------------------------------------------------------------------------
1 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
2 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
3 |
--------------------------------------------------------------------------------
/checkpoints/02/go.sum:
--------------------------------------------------------------------------------
1 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
2 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
3 |
--------------------------------------------------------------------------------
/checkpoints/03/go.sum:
--------------------------------------------------------------------------------
1 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
2 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
3 |
--------------------------------------------------------------------------------
/checkpoints/04/go.sum:
--------------------------------------------------------------------------------
1 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
2 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
3 |
--------------------------------------------------------------------------------
/checkpoints/05/go.sum:
--------------------------------------------------------------------------------
1 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
2 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
3 |
--------------------------------------------------------------------------------
/checkpoints/02/news/news.go:
--------------------------------------------------------------------------------
1 | package news
2 |
3 | import "net/http"
4 |
5 | type Client struct {
6 | http *http.Client
7 | key string
8 | PageSize int
9 | }
10 |
11 | func NewClient(httpClient *http.Client, key string, pageSize int) *Client {
12 | if pageSize > 100 {
13 | pageSize = 100
14 | }
15 |
16 | return &Client{httpClient, key, pageSize}
17 | }
18 |
--------------------------------------------------------------------------------
/checkpoints/01/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "html/template"
5 | "log"
6 | "net/http"
7 | "os"
8 |
9 | "github.com/joho/godotenv"
10 | )
11 |
12 | var tpl = template.Must(template.ParseFiles("index.html"))
13 |
14 | func indexHandler(w http.ResponseWriter, r *http.Request) {
15 | tpl.Execute(w, nil)
16 | }
17 |
18 | func main() {
19 | err := godotenv.Load()
20 | if err != nil {
21 | log.Println("Error loading .env file")
22 | }
23 |
24 | port := os.Getenv("PORT")
25 | if port == "" {
26 | port = "3000"
27 | }
28 |
29 | fs := http.FileServer(http.Dir("assets"))
30 |
31 | mux := http.NewServeMux()
32 | mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
33 | mux.HandleFunc("/", indexHandler)
34 | http.ListenAndServe(":"+port, mux)
35 | }
36 |
--------------------------------------------------------------------------------
/checkpoints/01/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | News App Demo
8 |
9 |
10 |
11 |
12 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/checkpoints/02/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | News App Demo
8 |
9 |
10 |
11 |
12 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/checkpoints/03/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | News App Demo
8 |
9 |
10 |
11 |
12 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT Licence
2 |
3 | Copyright (c) 2019 Ayooluwa Isaiah
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # News Application Demo
2 |
3 | Learn how to develop web applications with Go by building a News application.
4 |
5 | Here's what the [completed application](https://freshman-news.herokuapp.com/)
6 | looks like:
7 |
8 | 
9 |
10 | The code in this repo is meant to be a reference point for anyone following
11 | along with the [tutorial](https://freshman.tech/web-development-with-go/).
12 |
13 | ## Prerequisites
14 |
15 | - You need to have [Go](https://golang.org/dl/) installed on your computer. The
16 | version used to test the code in this repository is **1.15.3**.
17 |
18 | - Sign up for a [News API account](https://newsapi.org/register) and get your
19 | free API key.
20 |
21 | ## Get started
22 |
23 | - Clone this repository to your filesystem.
24 |
25 | ```bash
26 | $ git clone https://github.com/Freshman-tech/news-demo
27 | ```
28 |
29 | - Rename the `.env.example` file to `.env` and enter your News API Key.
30 | - `cd` into it and run the following command: `go build && ./news-demo` to start the server on port 3000.
31 | - Visit http://localhost:3000 in your browser.
32 |
--------------------------------------------------------------------------------
/checkpoints/02/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "html/template"
6 | "log"
7 | "net/http"
8 | "net/url"
9 | "os"
10 | "time"
11 |
12 | "github.com/freshman-tech/news-demo-starter-files/news"
13 | "github.com/joho/godotenv"
14 | )
15 |
16 | var tpl = template.Must(template.ParseFiles("index.html"))
17 |
18 | func indexHandler(w http.ResponseWriter, r *http.Request) {
19 | tpl.Execute(w, nil)
20 | }
21 |
22 | func searchHandler(newsapi *news.Client) http.HandlerFunc {
23 | return func(w http.ResponseWriter, r *http.Request) {
24 | u, err := url.Parse(r.URL.String())
25 | if err != nil {
26 | http.Error(w, err.Error(), http.StatusInternalServerError)
27 | return
28 | }
29 |
30 | params := u.Query()
31 | searchQuery := params.Get("q")
32 | page := params.Get("page")
33 | if page == "" {
34 | page = "1"
35 | }
36 |
37 | fmt.Println("Search Query is: ", searchQuery)
38 | fmt.Println("Page is: ", page)
39 | }
40 | }
41 |
42 | func main() {
43 | err := godotenv.Load()
44 | if err != nil {
45 | log.Println("Error loading .env file")
46 | }
47 |
48 | port := os.Getenv("PORT")
49 | if port == "" {
50 | port = "3000"
51 | }
52 |
53 | apiKey := os.Getenv("NEWS_API_KEY")
54 | if apiKey == "" {
55 | log.Fatal("Env: apiKey must be set")
56 | }
57 |
58 | myClient := &http.Client{Timeout: 10 * time.Second}
59 | newsapi := news.NewClient(myClient, apiKey, 20)
60 |
61 | fs := http.FileServer(http.Dir("assets"))
62 |
63 | mux := http.NewServeMux()
64 | mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
65 | mux.HandleFunc("/search", searchHandler(newsapi))
66 | mux.HandleFunc("/", indexHandler)
67 | http.ListenAndServe(":"+port, mux)
68 | }
69 |
--------------------------------------------------------------------------------
/checkpoints/03/news/news.go:
--------------------------------------------------------------------------------
1 | package news
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "net/url"
9 | "time"
10 | )
11 |
12 | type Article struct {
13 | Source struct {
14 | ID interface{} `json:"id"`
15 | Name string `json:"name"`
16 | } `json:"source"`
17 | Author string `json:"author"`
18 | Title string `json:"title"`
19 | Description string `json:"description"`
20 | URL string `json:"url"`
21 | URLToImage string `json:"urlToImage"`
22 | PublishedAt time.Time `json:"publishedAt"`
23 | Content string `json:"content"`
24 | }
25 |
26 | type Results struct {
27 | Status string `json:"status"`
28 | TotalResults int `json:"totalResults"`
29 | Articles []Article `json:"articles"`
30 | }
31 |
32 | type Client struct {
33 | http *http.Client
34 | key string
35 | PageSize int
36 | }
37 |
38 | func (c *Client) FetchEverything(query, page string) (*Results, error) {
39 | 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)
40 | resp, err := c.http.Get(endpoint)
41 | if err != nil {
42 | return nil, err
43 | }
44 |
45 | defer resp.Body.Close()
46 |
47 | body, err := ioutil.ReadAll(resp.Body)
48 | if err != nil {
49 | return nil, err
50 | }
51 |
52 | if resp.StatusCode != http.StatusOK {
53 | return nil, fmt.Errorf(string(body))
54 | }
55 |
56 | res := &Results{}
57 | return res, json.Unmarshal(body, res)
58 | }
59 |
60 | func NewClient(httpClient *http.Client, key string, pageSize int) *Client {
61 | if pageSize > 100 {
62 | pageSize = 100
63 | }
64 |
65 | return &Client{httpClient, key, pageSize}
66 | }
67 |
--------------------------------------------------------------------------------
/checkpoints/04/news/news.go:
--------------------------------------------------------------------------------
1 | package news
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "net/url"
9 | "time"
10 | )
11 |
12 | type Article struct {
13 | Source struct {
14 | ID interface{} `json:"id"`
15 | Name string `json:"name"`
16 | } `json:"source"`
17 | Author string `json:"author"`
18 | Title string `json:"title"`
19 | Description string `json:"description"`
20 | URL string `json:"url"`
21 | URLToImage string `json:"urlToImage"`
22 | PublishedAt time.Time `json:"publishedAt"`
23 | Content string `json:"content"`
24 | }
25 |
26 | type Results struct {
27 | Status string `json:"status"`
28 | TotalResults int `json:"totalResults"`
29 | Articles []Article `json:"articles"`
30 | }
31 |
32 | type Client struct {
33 | http *http.Client
34 | key string
35 | PageSize int
36 | }
37 |
38 | func (c *Client) FetchEverything(query, page string) (*Results, error) {
39 | 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)
40 | resp, err := c.http.Get(endpoint)
41 | if err != nil {
42 | return nil, err
43 | }
44 |
45 | defer resp.Body.Close()
46 |
47 | body, err := ioutil.ReadAll(resp.Body)
48 | if err != nil {
49 | return nil, err
50 | }
51 |
52 | if resp.StatusCode != http.StatusOK {
53 | return nil, fmt.Errorf(string(body))
54 | }
55 |
56 | res := &Results{}
57 | return res, json.Unmarshal(body, res)
58 | }
59 |
60 | func NewClient(httpClient *http.Client, key string, pageSize int) *Client {
61 | if pageSize > 100 {
62 | pageSize = 100
63 | }
64 |
65 | return &Client{httpClient, key, pageSize}
66 | }
67 |
--------------------------------------------------------------------------------
/checkpoints/04/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | News App Demo
8 |
9 |
10 |
11 |
12 |
30 |
31 |
32 | {{ range.Results.Articles }}
33 | -
34 |
44 |
45 |
46 | {{ end }}
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/checkpoints/03/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "html/template"
6 | "log"
7 | "net/http"
8 | "net/url"
9 | "os"
10 | "time"
11 |
12 | "github.com/freshman-tech/news-demo-starter-files/news"
13 | "github.com/joho/godotenv"
14 | )
15 |
16 | var tpl = template.Must(template.ParseFiles("index.html"))
17 |
18 | func indexHandler(w http.ResponseWriter, r *http.Request) {
19 | tpl.Execute(w, nil)
20 | }
21 |
22 | func searchHandler(newsapi *news.Client) http.HandlerFunc {
23 | return func(w http.ResponseWriter, r *http.Request) {
24 | u, err := url.Parse(r.URL.String())
25 | if err != nil {
26 | http.Error(w, err.Error(), http.StatusInternalServerError)
27 | return
28 | }
29 |
30 | params := u.Query()
31 | searchQuery := params.Get("q")
32 | page := params.Get("page")
33 | if page == "" {
34 | page = "1"
35 | }
36 |
37 | results, err := newsapi.FetchEverything(searchQuery, page)
38 | if err != nil {
39 | http.Error(w, err.Error(), http.StatusInternalServerError)
40 | return
41 | }
42 |
43 | fmt.Printf("%+v", results)
44 | }
45 | }
46 |
47 | func main() {
48 | err := godotenv.Load()
49 | if err != nil {
50 | log.Println("Error loading .env file")
51 | }
52 |
53 | port := os.Getenv("PORT")
54 | if port == "" {
55 | port = "3000"
56 | }
57 |
58 | apiKey := os.Getenv("NEWS_API_KEY")
59 | if apiKey == "" {
60 | log.Fatal("Env: apiKey must be set")
61 | }
62 |
63 | myClient := &http.Client{Timeout: 10 * time.Second}
64 | newsapi := news.NewClient(myClient, apiKey, 20)
65 |
66 | fs := http.FileServer(http.Dir("assets"))
67 |
68 | mux := http.NewServeMux()
69 | mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
70 | mux.HandleFunc("/search", searchHandler(newsapi))
71 | mux.HandleFunc("/", indexHandler)
72 | http.ListenAndServe(":"+port, mux)
73 | }
74 |
--------------------------------------------------------------------------------
/news/news.go:
--------------------------------------------------------------------------------
1 | package news
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "net/url"
9 | "time"
10 | )
11 |
12 | type Article struct {
13 | Source struct {
14 | ID interface{} `json:"id"`
15 | Name string `json:"name"`
16 | } `json:"source"`
17 | Author string `json:"author"`
18 | Title string `json:"title"`
19 | Description string `json:"description"`
20 | URL string `json:"url"`
21 | URLToImage string `json:"urlToImage"`
22 | PublishedAt time.Time `json:"publishedAt"`
23 | Content string `json:"content"`
24 | }
25 |
26 | func (a *Article) FormatPublishedDate() string {
27 | year, month, day := a.PublishedAt.Date()
28 | return fmt.Sprintf("%v %d, %d", month, day, year)
29 | }
30 |
31 | type Results struct {
32 | Status string `json:"status"`
33 | TotalResults int `json:"totalResults"`
34 | Articles []Article `json:"articles"`
35 | }
36 |
37 | type Client struct {
38 | http *http.Client
39 | key string
40 | PageSize int
41 | }
42 |
43 | func (c *Client) FetchEverything(query, page string) (*Results, error) {
44 | 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)
45 | resp, err := c.http.Get(endpoint)
46 | if err != nil {
47 | return nil, err
48 | }
49 |
50 | defer resp.Body.Close()
51 |
52 | body, err := ioutil.ReadAll(resp.Body)
53 | if err != nil {
54 | return nil, err
55 | }
56 |
57 | if resp.StatusCode != http.StatusOK {
58 | return nil, fmt.Errorf(string(body))
59 | }
60 |
61 | res := &Results{}
62 | return res, json.Unmarshal(body, res)
63 | }
64 |
65 | func NewClient(httpClient *http.Client, key string, pageSize int) *Client {
66 | if pageSize > 100 {
67 | pageSize = 100
68 | }
69 |
70 | return &Client{httpClient, key, pageSize}
71 | }
72 |
--------------------------------------------------------------------------------
/checkpoints/05/news/news.go:
--------------------------------------------------------------------------------
1 | package news
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "net/url"
9 | "time"
10 | )
11 |
12 | type Article struct {
13 | Source struct {
14 | ID interface{} `json:"id"`
15 | Name string `json:"name"`
16 | } `json:"source"`
17 | Author string `json:"author"`
18 | Title string `json:"title"`
19 | Description string `json:"description"`
20 | URL string `json:"url"`
21 | URLToImage string `json:"urlToImage"`
22 | PublishedAt time.Time `json:"publishedAt"`
23 | Content string `json:"content"`
24 | }
25 |
26 | func (a *Article) FormatPublishedDate() string {
27 | year, month, day := a.PublishedAt.Date()
28 | return fmt.Sprintf("%v %d, %d", month, day, year)
29 | }
30 |
31 | type Results struct {
32 | Status string `json:"status"`
33 | TotalResults int `json:"totalResults"`
34 | Articles []Article `json:"articles"`
35 | }
36 |
37 | type Client struct {
38 | http *http.Client
39 | key string
40 | PageSize int
41 | }
42 |
43 | func (c *Client) FetchEverything(query, page string) (*Results, error) {
44 | 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)
45 | resp, err := c.http.Get(endpoint)
46 | if err != nil {
47 | return nil, err
48 | }
49 |
50 | defer resp.Body.Close()
51 |
52 | body, err := ioutil.ReadAll(resp.Body)
53 | if err != nil {
54 | return nil, err
55 | }
56 |
57 | if resp.StatusCode != http.StatusOK {
58 | return nil, fmt.Errorf(string(body))
59 | }
60 |
61 | res := &Results{}
62 | return res, json.Unmarshal(body, res)
63 | }
64 |
65 | func NewClient(httpClient *http.Client, key string, pageSize int) *Client {
66 | if pageSize > 100 {
67 | pageSize = 100
68 | }
69 |
70 | return &Client{httpClient, key, pageSize}
71 | }
72 |
--------------------------------------------------------------------------------
/checkpoints/04/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "html/template"
6 | "log"
7 | "math"
8 | "net/http"
9 | "net/url"
10 | "os"
11 | "strconv"
12 | "time"
13 |
14 | "github.com/freshman-tech/news-demo-starter-files/news"
15 | "github.com/joho/godotenv"
16 | )
17 |
18 | var tpl = template.Must(template.ParseFiles("index.html"))
19 |
20 | type Search struct {
21 | Query string
22 | NextPage int
23 | TotalPages int
24 | Results *news.Results
25 | }
26 |
27 | func indexHandler(w http.ResponseWriter, r *http.Request) {
28 | buf := &bytes.Buffer{}
29 | err := tpl.Execute(buf, nil)
30 | if err != nil {
31 | http.Error(w, err.Error(), http.StatusInternalServerError)
32 | return
33 | }
34 |
35 | buf.WriteTo(w)
36 | }
37 |
38 | func searchHandler(newsapi *news.Client) http.HandlerFunc {
39 | return func(w http.ResponseWriter, r *http.Request) {
40 | u, err := url.Parse(r.URL.String())
41 | if err != nil {
42 | http.Error(w, err.Error(), http.StatusInternalServerError)
43 | return
44 | }
45 |
46 | params := u.Query()
47 | searchQuery := params.Get("q")
48 | page := params.Get("page")
49 | if page == "" {
50 | page = "1"
51 | }
52 |
53 | results, err := newsapi.FetchEverything(searchQuery, page)
54 | if err != nil {
55 | http.Error(w, err.Error(), http.StatusInternalServerError)
56 | return
57 | }
58 |
59 | nextPage, err := strconv.Atoi(page)
60 | if err != nil {
61 | http.Error(w, err.Error(), http.StatusInternalServerError)
62 | return
63 | }
64 |
65 | search := &Search{
66 | Query: searchQuery,
67 | NextPage: nextPage,
68 | TotalPages: int(math.Ceil(float64(results.TotalResults / newsapi.PageSize))),
69 | Results: results,
70 | }
71 |
72 | buf := &bytes.Buffer{}
73 | err = tpl.Execute(buf, search)
74 | if err != nil {
75 | http.Error(w, err.Error(), http.StatusInternalServerError)
76 | return
77 | }
78 |
79 | buf.WriteTo(w)
80 | }
81 | }
82 |
83 | func main() {
84 | err := godotenv.Load()
85 | if err != nil {
86 | log.Println("Error loading .env file")
87 | }
88 |
89 | port := os.Getenv("PORT")
90 | if port == "" {
91 | port = "3000"
92 | }
93 |
94 | apiKey := os.Getenv("NEWS_API_KEY")
95 | if apiKey == "" {
96 | log.Fatal("Env: apiKey must be set")
97 | }
98 |
99 | myClient := &http.Client{Timeout: 10 * time.Second}
100 | newsapi := news.NewClient(myClient, apiKey, 20)
101 |
102 | fs := http.FileServer(http.Dir("assets"))
103 |
104 | mux := http.NewServeMux()
105 | mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
106 | mux.HandleFunc("/search", searchHandler(newsapi))
107 | mux.HandleFunc("/", indexHandler)
108 | http.ListenAndServe(":"+port, mux)
109 | }
110 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | News App Demo
8 |
9 |
10 |
11 |
12 |
30 |
31 |
32 | {{ if .Results }}
33 | {{ if (gt .Results.TotalResults 0)}}
34 |
35 | About {{ .Results.TotalResults }} results were
36 | found. You are on page {{ .CurrentPage }} of
37 | {{ .TotalPages }}.
39 |
40 | {{ else if (ne .Query "") and (eq .Results.TotalResults 0) }}
41 |
42 | No results found for your query: {{ .Query }}.
44 |
45 | {{ end }}
46 | {{ end }}
47 |
48 |
49 |
50 | {{ range.Results.Articles }}
51 | -
52 |
62 |
63 |
64 |
65 | {{ end }}
66 |
67 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/checkpoints/05/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | News App Demo
8 |
9 |
10 |
11 |
12 |
30 |
31 |
32 | {{ if .Results }}
33 | {{ if (gt .Results.TotalResults 0)}}
34 |
35 | About {{ .Results.TotalResults }} results were
36 | found. You are on page {{ .CurrentPage }} of
37 | {{ .TotalPages }}.
39 |
40 | {{ else if and (ne .Query "") (eq .Results.TotalResults 0) }}
41 |
42 | No results found for your query: {{ .Query }}.
44 |
45 | {{ end }}
46 | {{ end }}
47 |
48 |
49 |
50 | {{ range.Results.Articles }}
51 | -
52 |
62 |
63 |
64 |
65 | {{ end }}
66 |
67 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "html/template"
6 | "log"
7 | "math"
8 | "net/http"
9 | "net/url"
10 | "os"
11 | "strconv"
12 | "time"
13 |
14 | "github.com/freshman-tech/news-demo/news"
15 | "github.com/joho/godotenv"
16 | )
17 |
18 | var tpl = template.Must(template.ParseFiles("index.html"))
19 |
20 | type Search struct {
21 | Query string
22 | NextPage int
23 | TotalPages int
24 | Results *news.Results
25 | }
26 |
27 | func (s *Search) IsLastPage() bool {
28 | return s.NextPage >= s.TotalPages
29 | }
30 |
31 | func (s *Search) CurrentPage() int {
32 | if s.NextPage == 1 {
33 | return s.NextPage
34 | }
35 |
36 | return s.NextPage - 1
37 | }
38 |
39 | func (s *Search) PreviousPage() int {
40 | return s.CurrentPage() - 1
41 | }
42 |
43 | func indexHandler(w http.ResponseWriter, r *http.Request) {
44 | buf := &bytes.Buffer{}
45 | err := tpl.Execute(buf, nil)
46 | if err != nil {
47 | http.Error(w, err.Error(), http.StatusInternalServerError)
48 | return
49 | }
50 |
51 | buf.WriteTo(w)
52 | }
53 |
54 | func searchHandler(newsapi *news.Client) http.HandlerFunc {
55 | return func(w http.ResponseWriter, r *http.Request) {
56 | u, err := url.Parse(r.URL.String())
57 | if err != nil {
58 | http.Error(w, err.Error(), http.StatusInternalServerError)
59 | return
60 | }
61 |
62 | params := u.Query()
63 | searchQuery := params.Get("q")
64 | page := params.Get("page")
65 | if page == "" {
66 | page = "1"
67 | }
68 |
69 | results, err := newsapi.FetchEverything(searchQuery, page)
70 | if err != nil {
71 | http.Error(w, err.Error(), http.StatusInternalServerError)
72 | return
73 | }
74 |
75 | nextPage, err := strconv.Atoi(page)
76 | if err != nil {
77 | http.Error(w, err.Error(), http.StatusInternalServerError)
78 | return
79 | }
80 |
81 | search := &Search{
82 | Query: searchQuery,
83 | NextPage: nextPage,
84 | TotalPages: int(math.Ceil(float64(results.TotalResults) / float64(newsapi.PageSize))),
85 | Results: results,
86 | }
87 |
88 | if ok := !search.IsLastPage(); ok {
89 | search.NextPage++
90 | }
91 |
92 | buf := &bytes.Buffer{}
93 | err = tpl.Execute(buf, search)
94 | if err != nil {
95 | http.Error(w, err.Error(), http.StatusInternalServerError)
96 | return
97 | }
98 |
99 | buf.WriteTo(w)
100 | }
101 | }
102 |
103 | func main() {
104 | err := godotenv.Load()
105 | if err != nil {
106 | log.Println("Error loading .env file")
107 | }
108 |
109 | port := os.Getenv("PORT")
110 | if port == "" {
111 | port = "3000"
112 | }
113 |
114 | apiKey := os.Getenv("NEWS_API_KEY")
115 | if apiKey == "" {
116 | log.Fatal("Env: apiKey must be set")
117 | }
118 |
119 | myClient := &http.Client{Timeout: 10 * time.Second}
120 | newsapi := news.NewClient(myClient, apiKey, 20)
121 |
122 | fs := http.FileServer(http.Dir("assets"))
123 |
124 | mux := http.NewServeMux()
125 | mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
126 | mux.HandleFunc("/search", searchHandler(newsapi))
127 | mux.HandleFunc("/", indexHandler)
128 | http.ListenAndServe(":"+port, mux)
129 | }
130 |
--------------------------------------------------------------------------------
/checkpoints/05/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "html/template"
6 | "log"
7 | "math"
8 | "net/http"
9 | "net/url"
10 | "os"
11 | "strconv"
12 | "time"
13 |
14 | "github.com/freshman-tech/news-demo-starter-files/news"
15 | "github.com/joho/godotenv"
16 | )
17 |
18 | var tpl = template.Must(template.ParseFiles("index.html"))
19 |
20 | type Search struct {
21 | Query string
22 | NextPage int
23 | TotalPages int
24 | Results *news.Results
25 | }
26 |
27 | func (s *Search) IsLastPage() bool {
28 | return s.NextPage >= s.TotalPages
29 | }
30 |
31 | func (s *Search) CurrentPage() int {
32 | if s.NextPage == 1 {
33 | return s.NextPage
34 | }
35 |
36 | return s.NextPage - 1
37 | }
38 |
39 | func (s *Search) PreviousPage() int {
40 | return s.CurrentPage() - 1
41 | }
42 |
43 | func indexHandler(w http.ResponseWriter, r *http.Request) {
44 | buf := &bytes.Buffer{}
45 | err := tpl.Execute(buf, nil)
46 | if err != nil {
47 | http.Error(w, err.Error(), http.StatusInternalServerError)
48 | return
49 | }
50 |
51 | buf.WriteTo(w)
52 | }
53 |
54 | func searchHandler(newsapi *news.Client) http.HandlerFunc {
55 | return func(w http.ResponseWriter, r *http.Request) {
56 | u, err := url.Parse(r.URL.String())
57 | if err != nil {
58 | http.Error(w, err.Error(), http.StatusInternalServerError)
59 | return
60 | }
61 |
62 | params := u.Query()
63 | searchQuery := params.Get("q")
64 | page := params.Get("page")
65 | if page == "" {
66 | page = "1"
67 | }
68 |
69 | results, err := newsapi.FetchEverything(searchQuery, page)
70 | if err != nil {
71 | http.Error(w, err.Error(), http.StatusInternalServerError)
72 | return
73 | }
74 |
75 | nextPage, err := strconv.Atoi(page)
76 | if err != nil {
77 | http.Error(w, err.Error(), http.StatusInternalServerError)
78 | return
79 | }
80 |
81 | search := &Search{
82 | Query: searchQuery,
83 | NextPage: nextPage,
84 | TotalPages: int(math.Ceil(float64(results.TotalResults / newsapi.PageSize))),
85 | Results: results,
86 | }
87 |
88 | if ok := !search.IsLastPage(); ok {
89 | search.NextPage++
90 | }
91 |
92 | buf := &bytes.Buffer{}
93 | err = tpl.Execute(buf, search)
94 | if err != nil {
95 | http.Error(w, err.Error(), http.StatusInternalServerError)
96 | return
97 | }
98 |
99 | buf.WriteTo(w)
100 | }
101 | }
102 |
103 | func main() {
104 | err := godotenv.Load()
105 | if err != nil {
106 | log.Println("Error loading .env file")
107 | }
108 |
109 | port := os.Getenv("PORT")
110 | if port == "" {
111 | port = "3000"
112 | }
113 |
114 | apiKey := os.Getenv("NEWS_API_KEY")
115 | if apiKey == "" {
116 | log.Fatal("Env: apiKey must be set")
117 | }
118 |
119 | myClient := &http.Client{Timeout: 10 * time.Second}
120 | newsapi := news.NewClient(myClient, apiKey, 20)
121 |
122 | fs := http.FileServer(http.Dir("assets"))
123 |
124 | mux := http.NewServeMux()
125 | mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
126 | mux.HandleFunc("/search", searchHandler(newsapi))
127 | mux.HandleFunc("/", indexHandler)
128 | http.ListenAndServe(":"+port, mux)
129 | }
130 |
--------------------------------------------------------------------------------
/assets/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | box-sizing: border-box;
3 | }
4 |
5 | *, *::before, *::after {
6 | box-sizing: inherit;
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
11 | :root {
12 | --light-green: #00ff00;
13 | --dark-green: #003b00;
14 | --dark-grey: #777;
15 | --light-grey: #dadce0;
16 | }
17 |
18 | body {
19 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
20 | }
21 |
22 | a {
23 | text-decoration: none;
24 | color: #333;
25 | }
26 |
27 | a:hover {
28 | text-decoration: underline;
29 | }
30 |
31 | a.button {
32 | border: 2px solid #004400;
33 | color: var(--dark-green);
34 | border-radius: 4px;
35 | padding: 6px 24px;
36 | font-size: 14px;
37 | font-weight: 400;
38 | }
39 |
40 | a.button:hover {
41 | text-decoration: none;
42 | background-color: var(--dark-green);
43 | color: var(--light-green);
44 | }
45 |
46 | header {
47 | width: 100%;
48 | height: 50px;
49 | position: fixed;
50 | top: 0;
51 | left: 0;
52 | right: 0;
53 | display: flex;
54 | justify-content: space-between;
55 | background-color: var(--light-green);
56 | padding: 5px 10px;
57 | align-items: center;
58 | }
59 |
60 | .logo {
61 | color: #002200;
62 | }
63 |
64 | form {
65 | height: calc(100% - 10px);
66 | }
67 |
68 | .search-input {
69 | width: 500px;
70 | height: 100%;
71 | border-radius: 4px;
72 | border-color: transparent;
73 | background-color: var(--dark-green);
74 | color: var(--light-green);
75 | font-size: 16px;
76 | line-height: 1.4;
77 | padding-left: 5px;
78 | }
79 |
80 | .container {
81 | width: 100%;
82 | max-width: 720px;
83 | margin: 0 auto;
84 | padding: 80px 20px 40px;
85 | }
86 |
87 | .result-count {
88 | color: var(--dark-grey);
89 | text-align: center;
90 | margin-bottom: 15px;
91 | }
92 |
93 | .search-results {
94 | list-style: none;
95 | }
96 |
97 | .news-article {
98 | display: flex;
99 | align-items: flex-start;
100 | margin-bottom: 30px;
101 | border: 1px solid var(--light-grey);
102 | padding: 15px;
103 | border-radius: 4px;
104 | justify-content: space-between;
105 | }
106 |
107 | .article-image {
108 | width: 200px;
109 | flex-grow: 0;
110 | flex-shrink: 0;
111 | margin-left: 20px;
112 | }
113 |
114 | .title {
115 | margin-bottom: 15px;
116 | }
117 |
118 | .description {
119 | color: var(--dark-grey);
120 | margin-bottom: 15px;
121 | }
122 |
123 | .metadata {
124 | display: flex;
125 | color: var(--dark-green);
126 | font-size: 14px;
127 | }
128 |
129 | .published-date::before {
130 | content: '\0000a0\002022\0000a0';
131 | margin: 0 3px;
132 | }
133 |
134 | .pagination {
135 | margin-top: 20px;
136 | }
137 |
138 | .previous-page {
139 | margin-right: 20px;
140 | }
141 |
142 | @media screen and (max-width: 550px) {
143 | header {
144 | flex-direction: column;
145 | height: auto;
146 | padding-bottom: 10px;
147 | }
148 |
149 | .logo {
150 | display: inline-block;
151 | margin-bottom: 10px;
152 | }
153 |
154 | form, .search-input {
155 | width: 100%;
156 | }
157 |
158 | .github-button {
159 | display: none;
160 | }
161 |
162 | .title {
163 | font-size: 18px;
164 | }
165 |
166 | .description {
167 | font-size: 14px;
168 | }
169 |
170 | .article-image {
171 | display: none;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/checkpoints/01/assets/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | box-sizing: border-box;
3 | }
4 |
5 | *, *::before, *::after {
6 | box-sizing: inherit;
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
11 | :root {
12 | --light-green: #00ff00;
13 | --dark-green: #003b00;
14 | --dark-grey: #777;
15 | --light-grey: #dadce0;
16 | }
17 |
18 | body {
19 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
20 | }
21 |
22 | a {
23 | text-decoration: none;
24 | color: #333;
25 | }
26 |
27 | a:hover {
28 | text-decoration: underline;
29 | }
30 |
31 | a.button {
32 | border: 2px solid #004400;
33 | color: var(--dark-green);
34 | border-radius: 4px;
35 | padding: 6px 24px;
36 | font-size: 14px;
37 | font-weight: 400;
38 | }
39 |
40 | a.button:hover {
41 | text-decoration: none;
42 | background-color: var(--dark-green);
43 | color: var(--light-green);
44 | }
45 |
46 | header {
47 | width: 100%;
48 | height: 50px;
49 | position: fixed;
50 | top: 0;
51 | left: 0;
52 | right: 0;
53 | display: flex;
54 | justify-content: space-between;
55 | background-color: var(--light-green);
56 | padding: 5px 10px;
57 | align-items: center;
58 | }
59 |
60 | .logo {
61 | color: #002200;
62 | }
63 |
64 | form {
65 | height: calc(100% - 10px);
66 | }
67 |
68 | .search-input {
69 | width: 500px;
70 | height: 100%;
71 | border-radius: 4px;
72 | border-color: transparent;
73 | background-color: var(--dark-green);
74 | color: var(--light-green);
75 | font-size: 16px;
76 | line-height: 1.4;
77 | padding-left: 5px;
78 | }
79 |
80 | .container {
81 | width: 100%;
82 | max-width: 720px;
83 | margin: 0 auto;
84 | padding: 80px 20px 40px;
85 | }
86 |
87 | .result-count {
88 | color: var(--dark-grey);
89 | text-align: center;
90 | margin-bottom: 15px;
91 | }
92 |
93 | .search-results {
94 | list-style: none;
95 | }
96 |
97 | .news-article {
98 | display: flex;
99 | align-items: flex-start;
100 | margin-bottom: 30px;
101 | border: 1px solid var(--light-grey);
102 | padding: 15px;
103 | border-radius: 4px;
104 | justify-content: space-between;
105 | }
106 |
107 | .article-image {
108 | width: 200px;
109 | flex-grow: 0;
110 | flex-shrink: 0;
111 | margin-left: 20px;
112 | }
113 |
114 | .title {
115 | margin-bottom: 15px;
116 | }
117 |
118 | .description {
119 | color: var(--dark-grey);
120 | margin-bottom: 15px;
121 | }
122 |
123 | .metadata {
124 | display: flex;
125 | color: var(--dark-green);
126 | font-size: 14px;
127 | }
128 |
129 | .published-date::before {
130 | content: '\0000a0\002022\0000a0';
131 | margin: 0 3px;
132 | }
133 |
134 | .pagination {
135 | margin-top: 20px;
136 | }
137 |
138 | .previous-page {
139 | margin-right: 20px;
140 | }
141 |
142 | @media screen and (max-width: 550px) {
143 | header {
144 | flex-direction: column;
145 | height: auto;
146 | padding-bottom: 10px;
147 | }
148 |
149 | .logo {
150 | display: inline-block;
151 | margin-bottom: 10px;
152 | }
153 |
154 | form, .search-input {
155 | width: 100%;
156 | }
157 |
158 | .github-button {
159 | display: none;
160 | }
161 |
162 | .title {
163 | font-size: 18px;
164 | }
165 |
166 | .description {
167 | font-size: 14px;
168 | }
169 |
170 | .article-image {
171 | display: none;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/checkpoints/02/assets/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | box-sizing: border-box;
3 | }
4 |
5 | *, *::before, *::after {
6 | box-sizing: inherit;
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
11 | :root {
12 | --light-green: #00ff00;
13 | --dark-green: #003b00;
14 | --dark-grey: #777;
15 | --light-grey: #dadce0;
16 | }
17 |
18 | body {
19 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
20 | }
21 |
22 | a {
23 | text-decoration: none;
24 | color: #333;
25 | }
26 |
27 | a:hover {
28 | text-decoration: underline;
29 | }
30 |
31 | a.button {
32 | border: 2px solid #004400;
33 | color: var(--dark-green);
34 | border-radius: 4px;
35 | padding: 6px 24px;
36 | font-size: 14px;
37 | font-weight: 400;
38 | }
39 |
40 | a.button:hover {
41 | text-decoration: none;
42 | background-color: var(--dark-green);
43 | color: var(--light-green);
44 | }
45 |
46 | header {
47 | width: 100%;
48 | height: 50px;
49 | position: fixed;
50 | top: 0;
51 | left: 0;
52 | right: 0;
53 | display: flex;
54 | justify-content: space-between;
55 | background-color: var(--light-green);
56 | padding: 5px 10px;
57 | align-items: center;
58 | }
59 |
60 | .logo {
61 | color: #002200;
62 | }
63 |
64 | form {
65 | height: calc(100% - 10px);
66 | }
67 |
68 | .search-input {
69 | width: 500px;
70 | height: 100%;
71 | border-radius: 4px;
72 | border-color: transparent;
73 | background-color: var(--dark-green);
74 | color: var(--light-green);
75 | font-size: 16px;
76 | line-height: 1.4;
77 | padding-left: 5px;
78 | }
79 |
80 | .container {
81 | width: 100%;
82 | max-width: 720px;
83 | margin: 0 auto;
84 | padding: 80px 20px 40px;
85 | }
86 |
87 | .result-count {
88 | color: var(--dark-grey);
89 | text-align: center;
90 | margin-bottom: 15px;
91 | }
92 |
93 | .search-results {
94 | list-style: none;
95 | }
96 |
97 | .news-article {
98 | display: flex;
99 | align-items: flex-start;
100 | margin-bottom: 30px;
101 | border: 1px solid var(--light-grey);
102 | padding: 15px;
103 | border-radius: 4px;
104 | justify-content: space-between;
105 | }
106 |
107 | .article-image {
108 | width: 200px;
109 | flex-grow: 0;
110 | flex-shrink: 0;
111 | margin-left: 20px;
112 | }
113 |
114 | .title {
115 | margin-bottom: 15px;
116 | }
117 |
118 | .description {
119 | color: var(--dark-grey);
120 | margin-bottom: 15px;
121 | }
122 |
123 | .metadata {
124 | display: flex;
125 | color: var(--dark-green);
126 | font-size: 14px;
127 | }
128 |
129 | .published-date::before {
130 | content: '\0000a0\002022\0000a0';
131 | margin: 0 3px;
132 | }
133 |
134 | .pagination {
135 | margin-top: 20px;
136 | }
137 |
138 | .previous-page {
139 | margin-right: 20px;
140 | }
141 |
142 | @media screen and (max-width: 550px) {
143 | header {
144 | flex-direction: column;
145 | height: auto;
146 | padding-bottom: 10px;
147 | }
148 |
149 | .logo {
150 | display: inline-block;
151 | margin-bottom: 10px;
152 | }
153 |
154 | form, .search-input {
155 | width: 100%;
156 | }
157 |
158 | .github-button {
159 | display: none;
160 | }
161 |
162 | .title {
163 | font-size: 18px;
164 | }
165 |
166 | .description {
167 | font-size: 14px;
168 | }
169 |
170 | .article-image {
171 | display: none;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/checkpoints/03/assets/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | box-sizing: border-box;
3 | }
4 |
5 | *, *::before, *::after {
6 | box-sizing: inherit;
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
11 | :root {
12 | --light-green: #00ff00;
13 | --dark-green: #003b00;
14 | --dark-grey: #777;
15 | --light-grey: #dadce0;
16 | }
17 |
18 | body {
19 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
20 | }
21 |
22 | a {
23 | text-decoration: none;
24 | color: #333;
25 | }
26 |
27 | a:hover {
28 | text-decoration: underline;
29 | }
30 |
31 | a.button {
32 | border: 2px solid #004400;
33 | color: var(--dark-green);
34 | border-radius: 4px;
35 | padding: 6px 24px;
36 | font-size: 14px;
37 | font-weight: 400;
38 | }
39 |
40 | a.button:hover {
41 | text-decoration: none;
42 | background-color: var(--dark-green);
43 | color: var(--light-green);
44 | }
45 |
46 | header {
47 | width: 100%;
48 | height: 50px;
49 | position: fixed;
50 | top: 0;
51 | left: 0;
52 | right: 0;
53 | display: flex;
54 | justify-content: space-between;
55 | background-color: var(--light-green);
56 | padding: 5px 10px;
57 | align-items: center;
58 | }
59 |
60 | .logo {
61 | color: #002200;
62 | }
63 |
64 | form {
65 | height: calc(100% - 10px);
66 | }
67 |
68 | .search-input {
69 | width: 500px;
70 | height: 100%;
71 | border-radius: 4px;
72 | border-color: transparent;
73 | background-color: var(--dark-green);
74 | color: var(--light-green);
75 | font-size: 16px;
76 | line-height: 1.4;
77 | padding-left: 5px;
78 | }
79 |
80 | .container {
81 | width: 100%;
82 | max-width: 720px;
83 | margin: 0 auto;
84 | padding: 80px 20px 40px;
85 | }
86 |
87 | .result-count {
88 | color: var(--dark-grey);
89 | text-align: center;
90 | margin-bottom: 15px;
91 | }
92 |
93 | .search-results {
94 | list-style: none;
95 | }
96 |
97 | .news-article {
98 | display: flex;
99 | align-items: flex-start;
100 | margin-bottom: 30px;
101 | border: 1px solid var(--light-grey);
102 | padding: 15px;
103 | border-radius: 4px;
104 | justify-content: space-between;
105 | }
106 |
107 | .article-image {
108 | width: 200px;
109 | flex-grow: 0;
110 | flex-shrink: 0;
111 | margin-left: 20px;
112 | }
113 |
114 | .title {
115 | margin-bottom: 15px;
116 | }
117 |
118 | .description {
119 | color: var(--dark-grey);
120 | margin-bottom: 15px;
121 | }
122 |
123 | .metadata {
124 | display: flex;
125 | color: var(--dark-green);
126 | font-size: 14px;
127 | }
128 |
129 | .published-date::before {
130 | content: '\0000a0\002022\0000a0';
131 | margin: 0 3px;
132 | }
133 |
134 | .pagination {
135 | margin-top: 20px;
136 | }
137 |
138 | .previous-page {
139 | margin-right: 20px;
140 | }
141 |
142 | @media screen and (max-width: 550px) {
143 | header {
144 | flex-direction: column;
145 | height: auto;
146 | padding-bottom: 10px;
147 | }
148 |
149 | .logo {
150 | display: inline-block;
151 | margin-bottom: 10px;
152 | }
153 |
154 | form, .search-input {
155 | width: 100%;
156 | }
157 |
158 | .github-button {
159 | display: none;
160 | }
161 |
162 | .title {
163 | font-size: 18px;
164 | }
165 |
166 | .description {
167 | font-size: 14px;
168 | }
169 |
170 | .article-image {
171 | display: none;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/checkpoints/04/assets/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | box-sizing: border-box;
3 | }
4 |
5 | *, *::before, *::after {
6 | box-sizing: inherit;
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
11 | :root {
12 | --light-green: #00ff00;
13 | --dark-green: #003b00;
14 | --dark-grey: #777;
15 | --light-grey: #dadce0;
16 | }
17 |
18 | body {
19 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
20 | }
21 |
22 | a {
23 | text-decoration: none;
24 | color: #333;
25 | }
26 |
27 | a:hover {
28 | text-decoration: underline;
29 | }
30 |
31 | a.button {
32 | border: 2px solid #004400;
33 | color: var(--dark-green);
34 | border-radius: 4px;
35 | padding: 6px 24px;
36 | font-size: 14px;
37 | font-weight: 400;
38 | }
39 |
40 | a.button:hover {
41 | text-decoration: none;
42 | background-color: var(--dark-green);
43 | color: var(--light-green);
44 | }
45 |
46 | header {
47 | width: 100%;
48 | height: 50px;
49 | position: fixed;
50 | top: 0;
51 | left: 0;
52 | right: 0;
53 | display: flex;
54 | justify-content: space-between;
55 | background-color: var(--light-green);
56 | padding: 5px 10px;
57 | align-items: center;
58 | }
59 |
60 | .logo {
61 | color: #002200;
62 | }
63 |
64 | form {
65 | height: calc(100% - 10px);
66 | }
67 |
68 | .search-input {
69 | width: 500px;
70 | height: 100%;
71 | border-radius: 4px;
72 | border-color: transparent;
73 | background-color: var(--dark-green);
74 | color: var(--light-green);
75 | font-size: 16px;
76 | line-height: 1.4;
77 | padding-left: 5px;
78 | }
79 |
80 | .container {
81 | width: 100%;
82 | max-width: 720px;
83 | margin: 0 auto;
84 | padding: 80px 20px 40px;
85 | }
86 |
87 | .result-count {
88 | color: var(--dark-grey);
89 | text-align: center;
90 | margin-bottom: 15px;
91 | }
92 |
93 | .search-results {
94 | list-style: none;
95 | }
96 |
97 | .news-article {
98 | display: flex;
99 | align-items: flex-start;
100 | margin-bottom: 30px;
101 | border: 1px solid var(--light-grey);
102 | padding: 15px;
103 | border-radius: 4px;
104 | justify-content: space-between;
105 | }
106 |
107 | .article-image {
108 | width: 200px;
109 | flex-grow: 0;
110 | flex-shrink: 0;
111 | margin-left: 20px;
112 | }
113 |
114 | .title {
115 | margin-bottom: 15px;
116 | }
117 |
118 | .description {
119 | color: var(--dark-grey);
120 | margin-bottom: 15px;
121 | }
122 |
123 | .metadata {
124 | display: flex;
125 | color: var(--dark-green);
126 | font-size: 14px;
127 | }
128 |
129 | .published-date::before {
130 | content: '\0000a0\002022\0000a0';
131 | margin: 0 3px;
132 | }
133 |
134 | .pagination {
135 | margin-top: 20px;
136 | }
137 |
138 | .previous-page {
139 | margin-right: 20px;
140 | }
141 |
142 | @media screen and (max-width: 550px) {
143 | header {
144 | flex-direction: column;
145 | height: auto;
146 | padding-bottom: 10px;
147 | }
148 |
149 | .logo {
150 | display: inline-block;
151 | margin-bottom: 10px;
152 | }
153 |
154 | form, .search-input {
155 | width: 100%;
156 | }
157 |
158 | .github-button {
159 | display: none;
160 | }
161 |
162 | .title {
163 | font-size: 18px;
164 | }
165 |
166 | .description {
167 | font-size: 14px;
168 | }
169 |
170 | .article-image {
171 | display: none;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/checkpoints/05/assets/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | box-sizing: border-box;
3 | }
4 |
5 | *, *::before, *::after {
6 | box-sizing: inherit;
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
11 | :root {
12 | --light-green: #00ff00;
13 | --dark-green: #003b00;
14 | --dark-grey: #777;
15 | --light-grey: #dadce0;
16 | }
17 |
18 | body {
19 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
20 | }
21 |
22 | a {
23 | text-decoration: none;
24 | color: #333;
25 | }
26 |
27 | a:hover {
28 | text-decoration: underline;
29 | }
30 |
31 | a.button {
32 | border: 2px solid #004400;
33 | color: var(--dark-green);
34 | border-radius: 4px;
35 | padding: 6px 24px;
36 | font-size: 14px;
37 | font-weight: 400;
38 | }
39 |
40 | a.button:hover {
41 | text-decoration: none;
42 | background-color: var(--dark-green);
43 | color: var(--light-green);
44 | }
45 |
46 | header {
47 | width: 100%;
48 | height: 50px;
49 | position: fixed;
50 | top: 0;
51 | left: 0;
52 | right: 0;
53 | display: flex;
54 | justify-content: space-between;
55 | background-color: var(--light-green);
56 | padding: 5px 10px;
57 | align-items: center;
58 | }
59 |
60 | .logo {
61 | color: #002200;
62 | }
63 |
64 | form {
65 | height: calc(100% - 10px);
66 | }
67 |
68 | .search-input {
69 | width: 500px;
70 | height: 100%;
71 | border-radius: 4px;
72 | border-color: transparent;
73 | background-color: var(--dark-green);
74 | color: var(--light-green);
75 | font-size: 16px;
76 | line-height: 1.4;
77 | padding-left: 5px;
78 | }
79 |
80 | .container {
81 | width: 100%;
82 | max-width: 720px;
83 | margin: 0 auto;
84 | padding: 80px 20px 40px;
85 | }
86 |
87 | .result-count {
88 | color: var(--dark-grey);
89 | text-align: center;
90 | margin-bottom: 15px;
91 | }
92 |
93 | .search-results {
94 | list-style: none;
95 | }
96 |
97 | .news-article {
98 | display: flex;
99 | align-items: flex-start;
100 | margin-bottom: 30px;
101 | border: 1px solid var(--light-grey);
102 | padding: 15px;
103 | border-radius: 4px;
104 | justify-content: space-between;
105 | }
106 |
107 | .article-image {
108 | width: 200px;
109 | flex-grow: 0;
110 | flex-shrink: 0;
111 | margin-left: 20px;
112 | }
113 |
114 | .title {
115 | margin-bottom: 15px;
116 | }
117 |
118 | .description {
119 | color: var(--dark-grey);
120 | margin-bottom: 15px;
121 | }
122 |
123 | .metadata {
124 | display: flex;
125 | color: var(--dark-green);
126 | font-size: 14px;
127 | }
128 |
129 | .published-date::before {
130 | content: '\0000a0\002022\0000a0';
131 | margin: 0 3px;
132 | }
133 |
134 | .pagination {
135 | margin-top: 20px;
136 | }
137 |
138 | .previous-page {
139 | margin-right: 20px;
140 | }
141 |
142 | @media screen and (max-width: 550px) {
143 | header {
144 | flex-direction: column;
145 | height: auto;
146 | padding-bottom: 10px;
147 | }
148 |
149 | .logo {
150 | display: inline-block;
151 | margin-bottom: 10px;
152 | }
153 |
154 | form, .search-input {
155 | width: 100%;
156 | }
157 |
158 | .github-button {
159 | display: none;
160 | }
161 |
162 | .title {
163 | font-size: 18px;
164 | }
165 |
166 | .description {
167 | font-size: 14px;
168 | }
169 |
170 | .article-image {
171 | display: none;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------