Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save meyu/d145255656b5adb4cba0ee714af5780f to your computer and use it in GitHub Desktop.
Save meyu/d145255656b5adb4cba0ee714af5780f to your computer and use it in GitHub Desktop.
exercise@freeCodeCamp: Show the Local Weather

purpose

For the challenge of freeCodeCamp's "Show the Local Weather", and for fun of playing with something cool.

build by

Dark Sky API

  • Dark Sky API is pretty good, though it needs to register before using it.
  • use CORS Anywhere for avoiding the CORS problem, but thought there must be a better way.
  • Hourly forecast is too detailed when I tried to show the information of next 24 hours later, so I hide the not-so-different-weather hour. Think it will be easier to understand.

Google Maps API

  • pick up the JavaScript API for showing a interactive map of user's location.
  • pick up the Geocoding API for showing the name of user's location. But there's a CORS problem when I trying to use Google API key to restrict the usage of this API, cause it was trigger by the client side. Luckly, Geocoding API can use without key, so the solution is still in the air.

ipinfo.io

  • ipinfo.io is great for lookup user's IP, and get a approximate location.
  • use it as a alternative plan when Geolocation API fails.

Bootstrap v4.0.0-alpha.6

  • design a screen-center layout with mulitple component was not so easy, but I got through and have lots of fun with the Flexbox feature.
  • Popovers is lovely, but I met trouble when trying to inject a Google Map into it. So I trigger the Google Maps API after the Popover shown, and patch a function to make the Popover disapear when click outside the Popover.

Skycons & Weather Icons v2.0.9 & Font Awesome v4.7.0

  • Skycons can pair perfectly with Dark Sky's forecast icon, and kinda cute.
  • Weather Icons has bunch of useful symbol for weather, it's a pity that it's no longer maintained.
  • Font Awesome is awesome, it benefit me this time for making a full screen loader without doing much css trick.

Google Fonts

  • Bubbler One is thin, concise,and not so formal. So that being a Flat-Design I can try .
  • tried to find a Chinese Web Font to pair with, but so hard to get one.

uiGradients

  • use uiGradients to pick up a calm background.
  • it's enjoyable when browsing the their website, so colorful and inspiring.

Toggle Switch

  • Switching between Fahrenheit and Celsius is needed, but found no style-I-like component in HTML 5 and Bootstrap 4.
  • Found Toggle Switch on w3schools, and customized it to fit my style. Now it looks so unique.

GitHub Gist

  • export this CodePen to my GitHub Gist for another way of code demonstration.
<!-- 第一全螢幕置中區 -->
<div class="container-fluid h-100 night">
<div class="h-100 d-flex justify-content-center align-items-center text-center lead">
<!-- loader -->
<div id="loader">
<i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i>
<span class="sr-only">Loading...</span>
</div>
<!-- error -->
<div id="noData" class="text-muted" hidden>
<i class="fa fa-wifi" aria-hidden="true"></i>
<span> loose connection</span>
</div>
<!-- 即時資訊區 -->
<div id="nowInfo" class="row after-loader" hidden>
<!-- 左 -->
<div class="col-sm-6 col-xs-12">
<div id="nowSummary" style='white-space:nowrap;'></div>
<canvas id="iconCurrent" width="150%">...connecting...</canvas>
</div>
<!-- 右 -->
<div class="col-sm-6 col-xs-12">
<span id="nowHumidity"></span>&nbsp;
<span id="nowPrecipIntensity"></span>
<div id="nowCelsius" class="celsius display-2"></div>
<div id="nowFahrenheit" class="fahrenheit display-2" hidden></div>
<!-- 切換鈕 -->
<label class="switch">
<input type="checkbox" onchange="switchFoC(this.checked)">
<div class="slider round">°F&nbsp;&nbsp;°C</div>
</label>
</div>
</div><!-- row -->
</div>
</div><!-- container-fluid -->
<!-- 細節資訊區 -->
<div id="detailInfo" class="container-fluid flip night" hidden>
<div class="d-flex justify-content-center align-items-center text-center lead flip">
<div class="row">
<!-- 地點 -->
<div class="h1 col-12 pb-5" style="margin:-15px 0 0 0;">
<a data-toggle="popover" data-placement="top" data-html="true"
data-content="<div id='urMap' style='width:100%;height:200px;'></div>
<div id='urPlace' class='small text-center text-muted'></div>">
<i class="fa fa-map-marker"></i>
</a>
</div>
<!-- 今日 -->
<div class="h1 col-12 pb-4">TODAY</div>
<div class="col-12">
<div class="col-12">
<canvas id="iconToday" width="100%" height="100%"></canvas>
</div>
<div class="col-12">
<span id="todayHumidity"></span>&nbsp;
<span id="todayPrecipIntensity"></span>
<div id="todayCelsius" class="celsius display-4"></div>
<div id="todayFahrenheit" class="fahrenheit display-4" hidden></div>
</div>
<div class="col-12 pt-5" id="todaySunTime"></div>
<div class="col-12 pb-5" id="todaySummary"></div>
</div>
<!-- 數小時 -->
<div class="h1 col-12 pt-5 pb-5">NEXT FEW HOURS</div>
<div id="nextFewHours" class="row col-12 justify-content-center"></div>
<!-- 數日 -->
<div class="h1 col-12 pt-5 pb-5">NEXT FEW DAYS</div>
<div id="nextFewDays" class="row col-12 justify-content-center"></div>
<div class="text-center col-12 pb-5" id="weekSummary"></div>
</div><!-- row -->
</div>
</div><!-- container-fluid -->
<!-- 置底區 -->
<div class="fixed-bottom">
<div class="float-right">
<button type="button" class="btn btn-outline-secondary fa fa-github-alt" data-toggle="modal" data-target="#readme">
</button>
</div>
</div>
<!-- readme Modal -->
<div class="modal fade" id="readme" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<a href="https://www.freecodecamp.com/meyu" target="_blank">
meyu@<i class="fa fa-free-code-camp" aria-hidden="true"></i>
</a>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<script src="https://gist.github.com/meyu/d145255656b5adb4cba0ee714af5780f.js"></script>
</div>
</div>
</div>
</div>
// 取得位置,並啟用 Popover
$(document).ready(function() {
getLocation();
$('[data-toggle="popover"]').popover()
});
// 使用者當下經緯度 (預設在桃園)
var d4Pos = {
lat : 24.9951273,
lon : 121.3176767,
name : "桃園市",
place_id : "ChIJS8IjmPkeaDQRF67g2Ty3XiI"
}
/////////////////////////////////////////////////////////
// 資料結繫 //////////////////////////////////////////////
////////////////////////////////////////////////////////
// 取得裝置的地理位置
function getLocation() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(position){
d4Pos.lat = position.coords.latitude;
d4Pos.lon = position.coords.longitude;
getDarkSkyApiData(d4Pos.lat, d4Pos.lon);
}, getIPLocation);
} else {
getIPLocation();
}
}
// 由裝置IP來取得使用者的地理位置。採用 ipinfo.io 方案 (https://ipinfo.io/developers)
function getIPLocation() {
var url = "https://ipinfo.io"
$.getJSON(url, function(data) {
var loc = data.loc.split(',');
d4Pos.lat = parseFloat(loc[0]);
d4Pos.lon = parseFloat(loc[1]);
})
.fail(function() {
alert(" Do not know where you are... \n Instead, I'll show the weather of my favorite place.");
})
.always(function() {
getDarkSkyApiData(d4Pos.lat, d4Pos.lon);
});
}
// 顯示所在地名稱,採用 Google Maps Geocoding API 方案 (https://developers.google.com/maps/documentation/geocoding/)
// 目前此方法,等同於由客戶端載入,故無法進行 Google API 的金鑰限制;暫時取消使用金鑰控制。
function showCity(lat, lon) {
var url = "https://maps.googleapis.com/maps/api/geocode/json?latlng=" + lat + "," + lon
$.getJSON(url, function( data ) {
if (data.status == "OK") {
d4Pos.name = data.results[1].formatted_address;
d4Pos.place_id = data.results[1].place_id;
$("#urPlace").html(d4Pos.name);
} else {
console.log(data);
}
})
.fail(function( data ) {
console.log("Geocoding API not OK");
});
}
// 顯示地圖,採用 Google Maps JavaScript API 方案 (https://developers.google.com/maps/documentation/javascript/tutorial)
function showMap(lat, lon) {
var uluru = {lat: lat, lng: lon};
var map = new google.maps.Map(document.getElementById('urMap'), {
zoom: 10, center: uluru
});
var marker = new google.maps.Marker({
position: uluru, map: map
});
}
// 取得天氣資訊,採用 Dark Sky API 方案 (https://darksky.net/dev/)
// 由客戶端讀取 API 會有 CORS 問題,所以暫時採用 CORS Anywhere 方案來介接 API (https://github.com/Rob--W/cors-anywhere/),但仍會有金鑰泄漏的問題
function getDarkSkyApiData(lat, lon) {
var avoidCORS = "https://cors-anywhere.herokuapp.com/";
var apiURL = "https://api.darksky.net/forecast";
var secretKey = "/ad50d1432876d180d315b159c4cab22d";
var latlon = "/" + lat + "," + lon
var paraExclude = "&exclude=minutely,alerts,flags"
var url = avoidCORS + apiURL + secretKey + latlon + "?" + paraExclude;
$.getJSON(url, function( data ) {
showNowInfo( data );
showTodayInfo( data );
showNextFewHoursInfo( data );
showNextFewDaysInfo( data );
//console.log(JSON.stringify(data, null, "\t"));
})
.fail(function() {
alert("I can not show you the weather from Dark Sky API... \nLost the connection with it.");
});
};
// 顯示當下天氣資訊
function showNowInfo( data ) {
// 氣象資訊
setSkycon(data.currently.icon,"iconCurrent");
$("#nowHumidity").html(getIconHtml("wi wi-humidity",data.currently.humidity*100,1));
$("#nowPrecipIntensity").html(getIconHtml("wi wi-umbrella",Math.round(data.currently.precipProbability*100)));
$("#nowFahrenheit").html(Math.round(data.currently.temperature));
$("#nowCelsius").html(F2C(data.currently.temperature));
// 晝夜效果
$(".container-fluid").addClass("night");
if (data.currently.time > data.daily.data[0].sunriseTime && data.currently.time < data.daily.data[0].sunsetTime) {
$(".container-fluid").removeClass("night");
$(".container-fluid").addClass("day");
}
// 登台
onStage(true);
}
// 顯示當日概況
function showTodayInfo( data ) {
// 氣象資訊
setSkycon(data.daily.data[0].icon,"iconToday");
$("#todaySummary").html(data.daily.data[0].summary);
$("#todayHumidity").html(getIconHtml("wi wi-humidity",data.daily.data[0].humidity*100,1));
$("#todayPrecipIntensity").html(getIconHtml("wi wi-umbrella",Math.round(data.daily.data[0].precipProbability*100)));
$("#todayFahrenheit").html(Math.round(data.daily.data[0].temperatureMin) + " ~ " + Math.round(data.daily.data[0].temperatureMax));
$("#todayCelsius").html(F2C(data.daily.data[0].temperatureMin) + " ~ " + F2C(data.daily.data[0].temperatureMax));
// 晝夜資訊
var sunrise = to24Time(toDate(data.daily.data[0].sunriseTime));
var sunset = to24Time(toDate(data.daily.data[0].sunsetTime));
$("#todaySunTime").html(getIconHtml("wi wi-sunrise",sunrise,1) + " " + sunset);
}
// 顯示未來24小時內之氣象資訊,但氣象變化不大之時段,會選擇性地不顯示
function showNextFewHoursInfo( data ) {
var howManyHours = 24
var dataHourly = data.hourly.data;
var lastInfo = {
te: data.currently.temperature,
pi: data.currently.precipProbability,
ic: data.currently.icon
}
for (i = 1; i <= howManyHours; i ++) {
if (i == 1 || i == howManyHours
|| Math.abs(F2C(lastInfo.te) - F2C(dataHourly[i].temperature)) > 1
|| Math.abs(lastInfo.pi-dataHourly[i].precipProbability) > 0.09
|| lastInfo.ic != dataHourly[i].icon)
{
lastInfo.te = dataHourly[i].temperature;
lastInfo.pi = dataHourly[i].precipProbability;
lastInfo.ic = dataHourly[i].icon;
var hour = toDate(dataHourly[i].time).getHours();
var html = "<div class='lead col-4 col-sm-3 col-md-2 col-lg-2 col-xl-1 pb-4'>"
+ "<div class='h2'>" + (hour%12 == 0 ? 12 : hour%12) + (hour>=12 ? 'PM' : 'AM') + "</div>"
+ "<canvas id='iconHourly" + i +"' width='75%' height='75%'></canvas>"
+ "<div>" + getIconHtml("wi wi-umbrella",Math.round(lastInfo.pi*100)) + "</div>"
+ "<div>"
+ "<span class='h2 celsius'>" + F2C(lastInfo.te) + "</span>"
+ "<span class='h2 fahrenheit' hidden>" + Math.round(lastInfo.te) + "</span>"
+ "</div>"
+ "</div>";
$("#nextFewHours").append(html);
setSkycon(lastInfo.ic,"iconHourly" + i);
}
}
}
// 顯示未來6天之氣象
function showNextFewDaysInfo( data ) {
var howManyDays = 6
$("#weekSummary").html(data.daily.summary);
var dataDaily = data.daily.data;
var inf = {
tn: dataDaily[0].temperatureMin,
tx: dataDaily[0].temperatureMax,
pi: dataDaily[0].precipProbability,
ic: dataDaily[0].icon
}
for (i = 1; i <= howManyDays; i ++) {
inf.tn = dataDaily[i].temperatureMin;
inf.tx = dataDaily[i].temperatureMax;
inf.pi = dataDaily[i].precipProbability;
inf.ic = dataDaily[i].icon;
var days = toDate(dataDaily[i].time).getDate();
var html = "<div class='lead col-4 col-sm-3 col-md-2 pb-4'>"
+ "<div class='h2'>" + days + toWeekDay(toDate(dataDaily[i].time)) + "</div>"
+ "<canvas id='iconDaily" + i +"' width='100%' height='100%'></canvas>"
+ "<div>" + getIconHtml("wi wi-umbrella",Math.round(inf.pi*100)) + "</div>"
+ "<div>"
+ "<span class='h2 celsius'>" + F2C(inf.tn) + "~" + F2C(inf.tx) + "</span>"
+ "<span class='h2 fahrenheit' hidden>" + Math.round(inf.tn) + "~" + Math.round(inf.tx) + "</span>"
+ "</div>"
+ "</div>";
$("#nextFewDays").append(html);
setSkycon(inf.ic,"iconDaily" + i);
}
}
/////////////////////////////////////////////////////////
// 人工觸發 //////////////////////////////////////////////
////////////////////////////////////////////////////////
// 點擊 Popover,並形成 map 所需 DOM 後,再載入地理資訊
$('[data-toggle="popover"]').on('shown.bs.popover', function () {
showMap(d4Pos.lat, d4Pos.lon);
showCity(d4Pos.lat, d4Pos.lon);
})
// 點擊 Popover 外部時,隱藏 Popover
$('body').on('click', function (e) {
$('[data-toggle=popover]').each(function () {
if (!$(this).is(e.target) && $(this).has(e.target).length === 0 && $('.popover').has(e.target).length === 0) {
$(this).popover('hide');
}
});
});
// 切換華氏<->攝氏資訊
function switchFoC(checked) {
if (checked) {
$(".fahrenheit").removeAttr("hidden");
$(".celsius").attr("hidden","true");
} else {
$(".celsius").removeAttr("hidden");
$(".fahrenheit").attr("hidden","true");
}
}
/////////////////////////////////////////////////////////
// 方便功能 //////////////////////////////////////////////
////////////////////////////////////////////////////////
// loader 登台效果
function onStage(tf) {
document.getElementById("loader").style.display = "none";
if (tf == true) {
// 登場
$("#nowInfo").removeAttr("hidden");
$("#detailInfo").removeAttr("hidden");
} else {
// 銘謝惠顧
$("#noData").removeAttr("hidden");
}
}
// 將華氏度換算為攝氏
function F2C(degree) {
return Math.round((degree - 32) * 5 / 9);
}
// 產生帶圖示的 html 詞塊
// 基礎圖示方案為 Font Awesome (http://fontawesome.io)
// 氣象圖示方案為 Weather Icons (https://erikflowers.github.io/weather-icons/)
function getIconHtml(icon,content,suffix) {
var iconExplain = '';
switch(icon) {
case 'wi wi-humidity':
iconExplain = 'Humidity(%)'
break;
case 'wi wi-umbrella':
iconExplain = 'The probability of precipitation occurring (%)'
break;
case 'wi wi-sunrise':
iconExplain = 'Sunrise & Sunset'
break;
}
var i = "<i class='fa-fw " + icon + "' data-toggle='tooltip' aria-label='" + iconExplain + "' title='" + iconExplain + "'></i>"
return (suffix == 1 ? content + " " + i : i + " " + content)
}
// 產生氣象動圖,方案為 Skycons (https://darkskyapp.github.io/skycons/),並利用 RawGit 來載入其 GitHub 的 js 檔 (https://rawgithub.com)
function setSkycon(iconName,targetID) {
var skycons = new Skycons({"color": "white"});
skycons.set(targetID, iconName);
skycons.play();
}
// 產生日期物件
function toDate( data ) {
var d = new Date(data * 1000);
return d
}
// 產出 24 小時制之時間字串
function to24Time( datetime ) {
function addZero(i) {
if (i < 10) {
i = "0" + i;
}
return i;
}
var h = addZero(datetime.getHours());
var m = addZero(datetime.getMinutes());
return h + ':' + m
}
// 產出星期標示
function toWeekDay( datetime ) {
var weekday = new Array(7);
weekday[0] = "SUN";
weekday[1] = "MON";
weekday[2] = "TUE";
weekday[3] = "WEN";
weekday[4] = "THU";
weekday[5] = "FRI";
weekday[6] = "SAT";
return weekday[datetime.getDay()];
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js"></script>
<script src="https://rawgithub.com/darkskyapp/skycons/master/skycons.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCJbPgOhdNGyNf5EfJNLNummupBxW3f5dU"></script>
/* 為了讓物件能於畫面居中舖路 */
html, body {
height: 100%;
color: white;
font-family: 'Bubbler One', sans-serif;
}
/* 微調Popover的視覺效果 */
.popover {
width:100%;
background-color:#DCE9F8;
}
.popover-content {
padding:0;
background-color:#DCE9F8;
text-shadow: 2px 2px 5px rgba(150, 150, 150, 0.8);
}
/* 晝夜主題 */
/* 採用 uiGradients 方案:https://uigradients.com/ */
.day {
background: linear-gradient(to bottom, #64b3f4, #c2e59c);
}
.night {
background: linear-gradient(to top, #4e4376, #2b5876);
}
.flip {
-webkit-transform: rotate(180deg); /* Chrome, Safari, Opera */
transform: rotate(180deg);
}
/* The switch - the box around the slider */
/* 參考 s3school 製作 (https://www.w3schools.com/howto/howto_css_switch.asp) */
.switch {
position: relative;
width: 60px;
height: 34px;
}
.switch input {display:none;}
.slider {
border: 1px solid white;
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 28px;
width: 28px;
left: 2px;
bottom: 2px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
.slider.round {
border-radius: 35px;
}
.slider.round:before {
border-radius: 50%;
}
/* after loader */
.after-loader {
position: relative;
-webkit-animation-name: animatebottom;
-webkit-animation-duration: 1s;
animation-name: afterLoader;
animation-duration: 1s
}
@-webkit-keyframes afterLoader {
from { bottom:-100px; opacity:0 }
to { bottom:0px; opacity:1 }
}
@keyframes afterLoader {
from{ bottom:-100px; opacity:0 }
to{ bottom:0; opacity:1 }
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/weather-icons/2.0.9/css/weather-icons.min.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css?family=Bubbler+One" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment