Skip to content

Instantly share code, notes, and snippets.

@bayareawebpro
Created September 16, 2019 23:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bayareawebpro/57f25956f187139cd8a2115f3b5a2eeb to your computer and use it in GitHub Desktop.
Save bayareawebpro/57f25956f187139cd8a2115f3b5a2eeb to your computer and use it in GitHub Desktop.
Detect users Timezone from javascript
We have awesome library moment.js in JS world which can do all sorts of things with time, parse, validate, manipulate, and display dates and times in any formatting, it has also support to handle the timezone.
Pull the following in libraries:
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.14/moment-timezone.min.js"></script>
Now if you check you chrome developer console you will have access to following methods provided by moment.
-> moment.tz.guess()
<- "Asia/Calcutta"
It’s giving me "Asia/Calcutta", in your case it should be your timezone which based on your computer’s timezone.
Now we can use this timezone value and add that to users table by passing this timezone from registration form.
Run php artisan make:auth to create the auth scaffolding. Now open the resources/views/layouts/app.blade.php and add above moment js library above the script tag.
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.14/moment-timezone-with-data-2012-2022.min.js"></script>
<script src="{{ asset('js/app.js') }}"></script>
@stack('js')
Now open the registration page resources/views/auth/register.blade.php and add a hidden input with guessed timezone as value, I am assuming you have jQuery on the page:
<form class="form-horizontal" method="POST" action="{{ route('register') }}">
{{ csrf_field() }}
<input type="hidden" name="tz" id="tz">
...
</form>
At the bottom of registration view, we can push this script to populate the input.
...
@endsection
@push('js')
<script>
$(function () {
// guess user timezone
$('#tz').val(moment.tz.guess())
})
</script>
@endpush
Now visit the registration page and inspect the form element, you should see timezone populated with your timezone:
timezone input
If you don’t want to use front end way of getting users timezone, we have other way is to use some IP to location service to guess users timezone.
Detect users Timezone from php
We will use FreeGeoIp.net to get the location with timezone info from users IP. It will return plenty of details along with timezone:
{
"ip":"66.102.0.0",
"country_code":"US",
"country_name":"United States",
"region_code":"CA",
"region_name":"California",
"city":"Mountain View",
"zip_code":"94043",
"time_zone":"America\/Los_Angeles",
"latitude":37.4192,
"longitude":-122.0574,
"metro_code":807
}
But for our use, we will only need the time_zone field.
Also if you are running your application on localhost you should set a testing IP address, to can get your current IP address by googling my ip and use as test ip.
Add users timezone into Database
Laravel app/Http/Controllers/Auth/RegisterController.php controller gives a registered method which will be called once registration is completed. This is good place to update the user row in database with the timezone value.
protected function registered(Request $request, $user)
{
// set timezone
$timezone = $this->getTimezone($request);
$user->timezone = $timezone;
$user->save();
}
protected function getClientIp(): string
{
$ip = \request()->ip();
return $ip == '127.0.0.1' ? '66.102.0.0' : $ip;
}
protected function getTimezone(Request $request)
{
if ($timezone = $request->get('tz')) {
return $timezone;
}
// fetch it from FreeGeoIp
$ip = $this->getClientIp();
try {
$response = json_decode(file_get_contents('http://freegeoip.net/json/' . $ip), true);
return array_get($response, 'time_zone');
} catch (\Exception $e) {}
}
Update the users table migration to add timezone field.
$table->string('password');
$table->string('timezone', 60);
Now its time to test, register a user and you should be seeing timezone is set with users record. You should also give option in users profile where user can select a timezone from dropdown, preferably from profile edit screen using DateTimeZone::listIdentifiers(DateTimeZone::ALL) to get the list.
Showing date in users timezone
We have everything setup now we can move on the next step on how to display the date time stored in the database as UTC to users current timezone, it can be done using momentjs or from server side using Carbon library which is used in Laravel, let’s see how we accomplish it in both ways.
Using Moment js
If your laravel app is working as API you will be returning a response as JSON which will be returning all the date mutated fields from Eloquent as UTC. Now to change it in users timezone using moment js is pretty simple:
let localTime = moment.utc("2018-01-22 04:09:31").local();
This will change the UTC time into your browsers local time, there are many momentjs wrapper libraries are present for this, for example, Vue-moment which gives a lot of other filters to work with date time.
Use Carbon on laravel
Another option will be to change the timezone using laravel backend, we have already stored the user’s timezone, now to change any timestamp from UTC to users local timezone you just need to do the following:
// on a date column
$user->created_at->timezone('Asia/Kolkata')->toDateTimeString()
// Directly on a Carbon instance
Carbon\Carbon::parse('2018-01-22 04:09:31')->timezone('Asia/Kolkata')->toDateTimeString()
Since we will be needing this timezone conversion on most of the eloquent model in laravel app, we should move this logic into a trait:
Trait for Local Date
This trait will give one helper function localize('created_at') on model which will accept the date field name and it will return carbon date instance in users timezone:
namespace App;
use Carbon\Carbon;
trait HasLocalDates {
/**
* Localize a date to users timezone
*
* @param null $dateField
* @return Carbon
*/
public function localize($dateField = null)
{
$dateValue = is_null($this->{$dateField}) ? Carbon::now() : $this->{$dateField};
return $this->inUsersTimezone($dateValue);
}
/**
* Change timezone of a carbon date
*
* @param $dateValue
* @return Carbon
*/
private function inUsersTimezone($dateValue): Carbon
{
$timezone = optional(auth()->user())->timezone ?? config('app.timezone');
return $this->asDateTime($dateValue)->timezone($timezone);
}
}
Now to use this trait in any Eloquent Model just use it like this:
class User extends Authenticatable
{
use Notifiable, HasLocalDates;
...
Formatted Date Trait
In many cases we want a formatted dates returned to us from backend API so we don’t need to do conversion in front end, we can add one more trait which will add formatted dates on all the model fields cast as date into a format you defined in config file or you can give option to the user to choose a format they want to see the date and time in using settings tutorial.
<?php
namespace App;
use Illuminate\Support\Carbon;
trait FormatsDate
{
/**
* All the fields other than eloquent model dates array you want to format
*
* @var array
*/
protected $formattedDates = [];
/**
* Flag to disable formatting on demand
*
* @var bool
*/
protected $noFormat = false;
/**
* Prefix which will be added to the fields for formatted date
*
* @var string
*/
protected $formattedFieldPrefix = 'local_';
/**
* Override the models toArray to append the formatted dates fields
*
* @return array
*/
public function toArray()
{
$data = parent::toArray();
if( $this->noFormat ) return $data;
foreach ($this->getFormattedDateFields() as $dateField) {
$data[$this->formattedFieldPrefix.$dateField] = $this->toDateObject($this->{$dateField});;
}
return $data;
}
/**
* Format time part of timestamp
*
* @param $dateValue
* @return string|null
*/
private function formattedDate($dateValue)
{
if( is_null($dateValue) ) return null;
return $this->inUsersTimezone($dateValue)
->format(config('setting.date_format'));
}
/**
* Format date part of timestamp
*
* @param $dateValue
* @return string|null
*/
private function formattedTime($dateValue)
{
if( is_null($dateValue) ) return null;
return $this->inUsersTimezone($dateValue)
->format(config('setting.time_format'));
}
/**
* Format date diff for humans
*
* @param $dateValue
* @return string|null
*/
private function formattedDiffForHumans($dateValue)
{
if( is_null($dateValue) ) return null;
return $this->inUsersTimezone($dateValue)
->diffForHumans();
}
/**
* Built a date object for serialization
*
* @param $dateValue
* @return array
*/
private function toDateObject($dateValue): array
{
return [
'date' => $this->formattedDate($dateValue),
'time' => $this->formattedTime($dateValue),
'for_human' => $this->formattedDiffForHumans($dateValue)
];
}
/**
* Return all the fields which needed formatted dates
*
* @return mixed
*/
private function getFormattedDateFields()
{
return array_merge($this->formattedDates, $this->getDates());
}
/**
* Setter for formatted dates fields array
*
* @param array $formattedDates
*/
public function setFormattedDates(array $formattedDates)
{
$this->formattedDates = $formattedDates;
}
/**
* Get the formatted date object for a field
*
* @param $field
* @return array
*/
public function toLocalTime($field = null )
{
$dateValue = is_null($this->{$field}) ? Carbon::now() : $this->{$field};
return $this->toDateObject($dateValue);
}
/**
* Disable formatting for the dates
*
* @return $this
*/
public function disableFormat()
{
$this->noFormat = true;
return $this;
}
/**
* Enable formatting for the dates
*
* @return $this
*/
public function enableFormat()
{
$this->noFormat = false;
return $this;
}
/**
* Get the timestamp in users timezone
*
* @param $dateValue
* @return Carbon
*/
private function inUsersTimezone($dateValue): Carbon
{
$timezone = optional(auth()->user())->timezone ?? config('app.timezone');
return $this->asDateTime($dateValue)
->timezone($timezone);
}
}
Now let’s add the format settings in a config file, create configs/setting.php and add followings:
return [
'date_format' => 'Y m d',
'time_format' => 'g:i a',
];
To test this out, let’s add FormatsDate on the Comment model.
class Comment extends Model
{
use FormatsDate;
protected $guarded = [];
}
Now if you return Comment model from route it will be automatically serialized to JSON and it will have created_at and updated_at in formatted result
formatted date
As you can see, res the lt doesn’t include the approved_at timestamp into formatted fields, to add that we need to cast approved_at field as date, add following in to Comment model and refresh the page:
class Comment extends Model
{
use FormatsDate;
protected $dates = ['approved_at'];
...
Now it has included approved_at also into formatted fields.
Conclusion
Now you have ways to manage timezone. Which one you choose it depends on your project. If you want to keep backend and frontend separate you should consider FormattedDate trait option or user moment.js to show the time in users timezone from frontend. But as most of Laravel project are mixed of front end Vue.js component and blade views its also very usable to have a handy method on Eloquent Model itself to get the dates in users timezone, for that you can use HasLocalDates trait. I hope you liked this post and let me know in the comments if you need any help or how you have solved this problem with timezone.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment