├── 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 |
13 | 14 |
15 | 23 |
24 | View on GitHub 29 |
30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /checkpoints/02/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | News App Demo 8 | 9 | 10 | 11 |
12 |
13 | 14 |
15 | 23 |
24 | View on GitHub 29 |
30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /checkpoints/03/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | News App Demo 8 | 9 | 10 | 11 |
12 |
13 | 14 |
15 | 23 |
24 | View on GitHub 29 |
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 | ![demo](https://ik.imagekit.io/freshman/news-demo_MrYio9GKlzSi.png) 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 |
13 | 14 |
15 | 23 |
24 | View on GitHub 29 |
30 |
31 | 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 |
13 | 14 |
15 | 23 |
24 | View on Github 29 |
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 | 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 |
13 | 14 |
15 | 23 |
24 | View on GitHub 29 |
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 | 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 | --------------------------------------------------------------------------------