├── .gitignore
├── go.mod
├── .apiConfig.example
├── README.md
├── index.html
├── main.go
└── assets
└── style.css
/.gitignore:
--------------------------------------------------------------------------------
1 | .apiConfig
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/hamzamaach/weather-tracker-go
2 |
3 | go 1.22.3
4 |
--------------------------------------------------------------------------------
/.apiConfig.example:
--------------------------------------------------------------------------------
1 | {
2 | "OpenWeatherMapApiKey":"[put your open weather map api key here]"
3 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Weather Tracker
2 |
3 | Weather Tracker is a web application that allows users to search for the weather conditions of a specific city. The application fetches data from the OpenWeatherMap API and displays it in a user-friendly format.
4 |
5 | ## Features
6 |
7 | - Search for the current weather of any city.
8 | - Display temperature in Celsius.
9 | - Show additional weather details such as humidity, wind speed, and rain possibility.
10 | - Display weather icons and country flags.
11 |
12 | ## Installation
13 |
14 | ### Prerequisites
15 |
16 | - Go (version 1.22 or later)
17 | - OpenWeatherMap API key
18 |
19 | ### Steps
20 |
21 | 1. Clone the repository:
22 |
23 | ```sh
24 | git clone https://github.com/hamzamaach/weather-tracker.git
25 | cd weather-tracker
26 | ```
27 |
28 | 2. Rename the `.apiConfig.example` file to `.apiConfig` and add your OpenWeatherMap API key:
29 |
30 | ```sh
31 | mv .apiConfig.example .apiConfig
32 | ```
33 |
34 | Edit the `.apiConfig` file to include your OpenWeatherMap API key:
35 |
36 | ```json
37 | {
38 | "OpenWeatherMapApiKey": "[put your open weather map api key here]"
39 | }
40 | ```
41 |
48 | 3. Run the application:
49 |
50 | ```sh
51 | go run main.go
52 | ```
53 |
54 | The application will be accessible at `http://localhost:8080`.
55 |
56 | ## Usage
57 |
58 | 1. Open your web browser and go to `http://localhost:8080`.
59 | 2. Enter the name of a city in the search box and click the search button.
60 | 3. The weather information for the city will be displayed.
61 |
62 | ## Project Structure
63 |
64 | - `main.go`: The main Go file containing the server and handler functions.
65 | - `assets/`: Directory containing static assets like CSS files.
66 | - `index.html`: The main HTML template for the web application.
67 | - `.apiConfig`: Configuration file for storing the OpenWeatherMap API key.
68 |
69 | ## API Endpoints
70 |
71 | - `/`: The main endpoint for searching and displaying weather information.
72 |
73 |
74 | ## Contributing
75 |
76 | 1. Fork the repository.
77 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`).
78 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`).
79 | 4. Push to the branch (`git push origin feature/AmazingFeature`).
80 | 5. Open a pull request.
81 |
82 | ## Acknowledgements
83 |
84 | - OpenWeatherMap for providing the weather data API.
85 | - Flagcdn for providing country flag images.
86 |
87 | ---
88 |
89 | Feel free to reach out if you have any questions or suggestions!
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Weather Tracker
10 |
11 |
12 |
13 |
27 |
28 | {{ if .NotFound }}
29 |
30 | City not found !
31 |
32 | {{ else }}
33 |
34 |
35 |
36 |
37 |
38 |
39 | {{ .WeatherData.Sys.LocalTime }}
40 | {{ .WeatherData.Name }}, {{ .WeatherData.Sys.Country }}
41 |
42 |
43 | {{ .WeatherData.Main.Celsius }}°C
44 | {{ .WeatherData.Weather.Description }}
45 |
46 | Humidity: {{ .WeatherData.Main.Humidity }}%
47 | Wind speed: {{ .WeatherData.Wind.SpeedKmh }} km/h
48 | Pressure: {{ .WeatherData.Main.Pressure }} hPa
49 |
50 |
51 |
52 | Sunrise
53 | {{ .WeatherData.Sys.SunriseTime }}
54 |
55 |
56 | Sunset
57 | {{ .WeatherData.Sys.SunsetTime }}
58 |
59 |
60 | {{ end }}
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "html/template"
8 | "net/http"
9 | "os"
10 | "strconv"
11 | "strings"
12 | "time"
13 | )
14 |
15 | type apiConfigData struct {
16 | OpenWeatherMapApiKey string `json:"OpenWeatherMapApiKey"`
17 | }
18 |
19 | type weatherData struct {
20 | Name string `json:"name"`
21 | TimeZone int `json:"timezone"`
22 | Sys struct {
23 | Country string `json:"country"`
24 | Flag string
25 | Sunrise int64 `json:"sunrise"`
26 | Sunset int64 `json:"sunset"`
27 | SunriseTime string
28 | SunsetTime string
29 | LocalTime string
30 | } `json:"sys"`
31 | Main struct {
32 | Kelvin float64 `json:"temp"`
33 | KelvinLike float64 `json:"feels_like"`
34 | Humidity float64 `json:"humidity"`
35 | Pressure int `json:"pressure"`
36 | Celsius int
37 | CelsiusLike int
38 | } `json:"main"`
39 | WeatherAsSlice []struct {
40 | Description string `json:"description"`
41 | Icon string `json:"icon"`
42 | } `json:"weather"`
43 | Weather struct {
44 | Description string
45 | Icon string
46 | }
47 | Wind struct {
48 | Speed float64 `json:"speed"`
49 | SpeedKmh string
50 | } `json:"wind"`
51 | }
52 |
53 | func loadApiConfig(filename string) (apiConfigData, error) {
54 | bytes, err := os.ReadFile(filename)
55 | if err != nil {
56 | return apiConfigData{}, err
57 | }
58 | var c apiConfigData
59 | err = json.Unmarshal(bytes, &c)
60 | if err != nil {
61 | return apiConfigData{}, err
62 | }
63 | return c, nil
64 | }
65 |
66 | func getWindDescription(description string, feelsLike int, speed float64) string {
67 | var wind string
68 | speedKmh := speed * 3.6
69 | switch {
70 | case speedKmh < 1.5:
71 | wind = "Calm"
72 | case speedKmh < 5.5:
73 | wind = "Light Breeze"
74 | case speedKmh < 11.0:
75 | wind = "Gentle Breeze"
76 | case speedKmh < 19.0:
77 | wind = "Moderate Breeze"
78 | case speedKmh < 28.0:
79 | wind = "Fresh Breeze"
80 | case speedKmh < 38.0:
81 | wind = "Strong Breeze"
82 | case speedKmh < 49.0:
83 | wind = "Near Gale"
84 | case speedKmh < 61.0:
85 | wind = "Gale"
86 | case speedKmh < 74.0:
87 | wind = "Severe Gale"
88 | case speedKmh < 88.0:
89 | wind = "Storm"
90 | case speedKmh < 102.0:
91 | wind = "Violent Storm"
92 | default:
93 | wind = "Hurricane"
94 | }
95 | return fmt.Sprintf("Feels like %d°C. %s. %s", feelsLike, description, wind)
96 | }
97 |
98 | func (wd *weatherData) setCurrentTime() {
99 | location := time.FixedZone("Local Time", wd.TimeZone)
100 | wd.Sys.LocalTime = time.Now().In(location).Format("Jan 02, 03:04pm")
101 | }
102 |
103 | // RenderTemplate renders the specified template with data
104 | func RenderTemplate(w http.ResponseWriter, tmpl string, data interface{}) {
105 | t, err := template.ParseFiles(tmpl)
106 | if err != nil {
107 | fmt.Println("Error: ", err)
108 | return
109 | }
110 | err = t.ExecuteTemplate(w, tmpl, data)
111 | if err != nil {
112 | fmt.Println("Error executing template: ", err)
113 | }
114 | }
115 |
116 | func index(w http.ResponseWriter, r *http.Request) {
117 | city := r.URL.Query().Get("city")
118 | if city == "" {
119 | city = "oujda"
120 | }
121 | data, err := query(city)
122 | if err != nil {
123 | if err.Error() == "404" {
124 | // http.Error(w, "City not found", http.StatusNotFound)
125 | w.WriteHeader(http.StatusNotFound)
126 | RenderTemplate(w, "index.html", map[string]interface{}{
127 | "NotFound": true,
128 | "City": city,
129 | })
130 | } else {
131 | http.Error(w, "Error querying weather data", http.StatusInternalServerError)
132 | }
133 | return
134 | }
135 | RenderTemplate(w, "index.html", map[string]interface{}{
136 | "WeatherData": data,
137 | "City": city,
138 | })
139 | }
140 |
141 | func query(city string) (weatherData, error) {
142 | apiConfig, err := loadApiConfig(".apiConfig")
143 | if err != nil {
144 | return weatherData{}, err
145 | }
146 | resp, err := http.Get("https://api.openweathermap.org/data/2.5/weather?q=" + city + "&appid=" + apiConfig.OpenWeatherMapApiKey)
147 | if err != nil {
148 | return weatherData{}, err
149 | }
150 | defer resp.Body.Close()
151 |
152 | if resp.StatusCode == http.StatusNotFound {
153 | return weatherData{}, errors.New("404")
154 | }
155 |
156 | var data weatherData
157 | if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
158 | return weatherData{}, err
159 | }
160 |
161 | // Convert temperatures from Kelvin to Celsius
162 | data.Main.Celsius = int(data.Main.Kelvin - 273.15)
163 | data.Main.CelsiusLike = int(data.Main.KelvinLike - 273.15)
164 | data.Weather.Description = getWindDescription(data.WeatherAsSlice[0].Description, data.Main.CelsiusLike, data.Wind.Speed)
165 |
166 | data.Weather.Icon = "https://openweathermap.org/img/wn/" + data.WeatherAsSlice[0].Icon + "@2x.png"
167 | data.Sys.Flag = "https://flagcdn.com/16x12/" + strings.ToLower(data.Sys.Country) + ".png"
168 | data.Wind.SpeedKmh = strconv.FormatFloat(data.Wind.Speed*3.6, 'f', 1, 64)
169 | data.Sys.SunriseTime = time.Unix(data.Sys.Sunrise, 0).Format("03:04pm")
170 | data.Sys.SunsetTime = time.Unix(data.Sys.Sunset, 0).Format("03:04pm")
171 |
172 | data.setCurrentTime()
173 | return data, nil
174 | }
175 |
176 | func main() {
177 | fs := http.FileServer(http.Dir("./assets"))
178 | http.Handle("/assets/", http.StripPrefix("/assets/", fs))
179 | http.HandleFunc("/", index)
180 | http.ListenAndServe(":8080", nil)
181 | }
182 |
--------------------------------------------------------------------------------
/assets/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-family: 'Roboto Slab', sans-serif;
3 | }
4 |
5 | body {
6 | background: #55D6F4;
7 | color: #313E48;
8 | }
9 |
10 | img {
11 | max-width: 100%;
12 | }
13 |
14 | .box {
15 | margin: 1em auto;
16 | background: #f0f0f0;
17 | padding: 2em 0;
18 | width: 50%;
19 | border-radius: 1rem;
20 | box-shadow: 0px 1px 7px rgba(0, 0, 0, 0.25);
21 | }
22 |
23 | .box:hover {
24 | box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.5);
25 | }
26 |
27 | input:focus {
28 | outline: none;
29 | }
30 |
31 |
32 | .box h1 {
33 | font-size: 2em;
34 | text-align: center;
35 | }
36 |
37 | .box .temp {
38 | font-size: 7em;
39 | line-height: 1;
40 | text-align: center;
41 | display: block;
42 | padding-left: 30px;
43 | margin-bottom: 24px;
44 | }
45 |
46 | .box .high-low {
47 | font-size: 2em;
48 | color: lighten(#313E48, 20%);
49 | text-align: center;
50 | display: block;
51 | font-weight: 100;
52 | }
53 |
54 | .weather .icon {
55 | position: relative;
56 | margin: 0 auto;
57 | }
58 |
59 | .spin {
60 | position: absolute;
61 | top: 16px;
62 | right: 18px;
63 | text-align: center;
64 | width: 65px;
65 | }
66 |
67 | .city {
68 | margin-top: 0px;
69 | }
70 |
71 | .local-time {
72 | color: orangered;
73 | margin-top: 25px;
74 | text-align: center;
75 | display: block;
76 | }
77 |
78 | .details-div {
79 | display: grid;
80 | grid-template-columns: 1fr 1fr;
81 | grid-gap: 10px;
82 | border-left: solid orangered 3px;
83 | margin: 1rem 0 0 6rem;
84 | padding-left: 1rem;
85 | }
86 |
87 | .details {
88 | display: block;
89 | }
90 | .sun{
91 | text-align: center;
92 | display: flex;
93 | gap: 25px;
94 | margin: 1rem 0 0 0;
95 | justify-content: end;
96 | padding-right: 2rem;
97 | }
98 | .sun-title {
99 | display: block;
100 | color: orangered;
101 | font-size: 0.8rem;
102 | margin: auto;
103 | }
104 | .sun-value{
105 | display: block;
106 | margin-top: 7px;
107 | font-weight: 600;
108 | }
109 |
110 | .bubble.black {
111 | background-color: #8f8f8f;
112 | position: relative;
113 | width: 100px;
114 | height: 100px;
115 | padding: 0px;
116 | border-radius: 50px;
117 | }
118 |
119 | .bubble.black:after {
120 | content: "";
121 | position: absolute;
122 | bottom: -14px;
123 | left: 14px;
124 | border-style: solid;
125 | border-width: 29px 15px 0;
126 | border-color: #8f8f8f transparent;
127 | display: block;
128 | width: 0;
129 | z-index: 1;
130 | transform: rotate(30deg);
131 | }
132 |
133 | .spin img {
134 | animation: spin 5s linear infinite;
135 | animation-play-state: paused;
136 | }
137 |
138 | .box:hover .spin img {
139 | animation-play-state: running;
140 | }
141 |
142 | .tb {
143 | display: table;
144 | width: 100%;
145 | }
146 |
147 | .td {
148 | display: table-cell;
149 | vertical-align: middle;
150 | }
151 |
152 | input,
153 | button {
154 | color: #fff;
155 | font-family: Nunito;
156 | padding: 0;
157 | margin: 0;
158 | border: 0;
159 | background-color: transparent;
160 | }
161 |
162 | #cover {
163 | width: 550px;
164 | padding: 35px;
165 | margin: auto;
166 | background-color: orangered;
167 | border-radius: 20px;
168 | box-shadow: 0 10px 40px orangered, 0 0 0 20px #ffffffeb;
169 | transform: scale(0.6);
170 | }
171 |
172 | form {
173 | height: 96px;
174 | }
175 |
176 | input[type="text"] {
177 | width: 100%;
178 | height: 96px;
179 | font-size: 60px;
180 | line-height: 1;
181 | }
182 |
183 | input[type="text"]::placeholder {
184 | color: #e16868;
185 | }
186 |
187 | #s-cover {
188 | width: 1px;
189 | padding-left: 35px;
190 | }
191 |
192 | button {
193 | position: relative;
194 | display: block;
195 | width: 84px;
196 | height: 96px;
197 | cursor: pointer;
198 | }
199 |
200 | #s-circle {
201 | position: relative;
202 | top: -8px;
203 | left: 0;
204 | width: 43px;
205 | height: 43px;
206 | margin-top: 0;
207 | border-width: 15px;
208 | border: 15px solid #fff;
209 | background-color: transparent;
210 | border-radius: 50%;
211 | transition: 0.5s ease all;
212 | }
213 |
214 | .not-found {
215 | display: flex;
216 | justify-content: center;
217 | }
218 |
219 | button span {
220 | position: absolute;
221 | top: 68px;
222 | left: 43px;
223 | display: block;
224 | width: 45px;
225 | height: 15px;
226 | background-color: transparent;
227 | border-radius: 10px;
228 | transform: rotateZ(52deg);
229 | transition: 0.5s ease all;
230 | }
231 |
232 | button span:before,
233 | button span:after {
234 | content: "";
235 | position: absolute;
236 | bottom: 0;
237 | right: 0;
238 | width: 45px;
239 | height: 15px;
240 | background-color: #fff;
241 | border-radius: 10px;
242 | transform: rotateZ(0);
243 | transition: 0.5s ease all;
244 | }
245 |
246 | #s-cover:hover #s-circle {
247 | top: -1px;
248 | width: 67px;
249 | height: 15px;
250 | border-width: 0;
251 | background-color: #fff;
252 | border-radius: 20px;
253 | }
254 |
255 | #s-cover:hover span {
256 | top: 50%;
257 | left: 56px;
258 | width: 25px;
259 | margin-top: -9px;
260 | transform: rotateZ(0);
261 | }
262 |
263 | #s-cover:hover button span:before {
264 | bottom: 11px;
265 | transform: rotateZ(52deg);
266 | }
267 |
268 | #s-cover:hover button span:after {
269 | bottom: -11px;
270 | transform: rotateZ(-52deg);
271 | }
272 |
273 | #s-cover:hover button span:before,
274 | #s-cover:hover button span:after {
275 | right: -6px;
276 | width: 40px;
277 | background-color: #fff;
278 | }
--------------------------------------------------------------------------------