├── .apiConfig.example ├── .gitignore ├── README.md ├── assets └── style.css ├── go.mod ├── index.html └── main.go /.apiConfig.example: -------------------------------------------------------------------------------- 1 | { 2 | "OpenWeatherMapApiKey":"[put your open weather map api key here]" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .apiConfig -------------------------------------------------------------------------------- /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! -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hamzamaach/weather-tracker-go 2 | 3 | go 1.22.3 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Weather Tracker 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 | 23 |
24 |
25 |
26 |
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 | {{.WeatherData.Sys.Country}} 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 | --------------------------------------------------------------------------------