JavaScript - Parse UTC Date - javascript

How can I parse a simple date string and let JavaScript know that it's actually a UTC date? Currently, if I do new Date('2015-08-27') it converts it to my timezone.

You can do append 'T00:00:00.000Z' to make the time zone specific (Z indicates UTC)
new Date('2015-08-27' + 'T00:00:00.000Z')
Note that new Date('2015-08-27') is treated differently in ES5 (UTC) vs. ES6 (Local), so you can't expect it any correction to be work consistently if you were planning to to hard code it (i.e. don't do it)
Also, do note that your console.log might show you the local time corresponding to the UTC time the expression evaluates to (that tends to throw you off a bit if you are expecting UTC to be at the end for expression that evaluate to UTC times and your local time zone at the end for those that evaluate to your local time). For instance
new Date('2015-08-27T00:00:00.000Z')
could show
Thu Aug 27 2015 1:00:00 GMT+100
which is the same as
Thu Aug 27 2015 00:00:00 UTC

In some cases, while other solutions don't work, adding GMT will help:
new Date('July 11, 2022, 16:22:14 PM' + ' GMT')

This might be obvious to most, but I got stumped for a few seconds because my string already had hh:mm:ss, so it required a little bit of string manipulation.
var d = '2022-09-14 13:20:31';
d = d.split(' ').join('T')+'Z';
var date = new Date(d);
console.log(date);
This version is more verbose, but feels sturdier to me.
var d = '2022-09-14 13:20:31';
var [yyyy, mm, dd, hh, m, s] = d.split(/[^\d]+/);
var date = new Date();
date.setUTCFullYear(+yyyy);
date.setUTCMonth(mm-1);
date.setUTCDate(+dd);
date.setUTCHours(+hh);
date.setUTCMinutes(+m);
date.setUTCSeconds(+s);
console.log(date);

Here is what I would do.
var current = new Date();
var utcDate = new Date(current.getTime() + current.getTimezoneOffset() * 60000);

Related

Javascript dates: a nightmare

I understand that dealing with dates, in any environment, could be quite confusing, but I'm in a nightmare with a function that should be a trivial job.
I want to manipulate in different ways some dates, but I get errors or wrong results.
I report hereafter a very simple example made to test the execution; the goal here is to get the current month beginning date, just to show what happens:
function DateAdjust() {
var newdate = new Date(); //1: 2018-12-12T21:00:20.099Z
newdate = newdate.setDate(1); //2: 1543698020099
newdate=Date(newdate); //3: Wed Dec 12 2018 21:01:43 GMT+0000 (Ora standard dell’Europa occidentale)
var d = newdate.getDate(); //4: newdate.getDate is not a function
}
4 lines, 3 unexpected results (as shown by Firefox's debugger):
1. the starting date has no day-of-week and no timezone
2. setting the day, result is transformed in milliseconds (why?); I do not know if it is correct.
3. reconverting in string gives the original date, unmodified (why?) but with week day and timezone
4. trying to get the day value an error is thrown (why?)
My environment:
Win 7 32bits SP1
Firefox 63.0.3 (32 bit)
jquery-2.2.4.min.js
I know these questions are boring, but hope someone will find few minutes to clear my mind.
Regarding line 1, the Z at the end is the timezone designation for UTC in ISO 8601 (see Wikipedia).
If the time is in UTC, add a Z directly after the time without a space. Z is the zone designator for the zero UTC offset. "09:30 UTC" is therefore represented as "09:30Z" or "0930Z". "14:45:15 UTC" would be "14:45:15Z" or "144515Z".
Regarding line 2 see the MDN article on setDate (emphasis mine):
The number of milliseconds between 1 January 1970 00:00:00 UTC and the given date (the Date object is also changed in place).
So you can see the 'correct' behavior you probably expect simply by ignoring the return value:
var newdate = new Date(); //1: 2018-12-12T21:00:20.099Z
newdate.setDate(1); //2: 1543698020099
console.log(newdate); //3: 2018-12-01T21:00:20.099Z
Regarding line 3, see MDN article on Date (emphasis mine):
Note: JavaScript Date objects can only be instantiated by calling
JavaScript Date as a constructor: calling it as a regular function
(i.e. without the new operator) will return a string rather than a
Date object; unlike other JavaScript object types, JavaScript Date
objects have no literal syntax.
Regarding line 4, the above also explains this error, since newdate is now a string rather than a Date object.
For what it's worth, I agree with the other commenters. JavaScript's date functions are pretty messy compared to many other modern languages. I strongly recommend using a library like moment, luxon, or date-fns. It'll make your life much easier.
I do recommend using moment.js
But there are 2 problems with your code:
1-
newdate = newdate.setDate(1);
setDate mutates newDate in place, and return it in miliseconds, not a new Date object. If you just want to set the date, do this instead:
newdate.setDate(1);
2-
newdate=Date(newdate);
Not realy sure why you are trying to get a new Date object, but you need the new, otherwise it will just be a string
newdate= new Date(newdate);
Fixing problem 1 should eliminate the need for the code of problem 2
var newdate = new Date(); // 1
console.log(typeof newdate, newdate); // object Wed Dec 12 2018 23:00:44 GMT+0200 (Eastern European Standard Time)
newdate = newdate.setDate(1); // 2
console.log(typeof newdate, newdate); //number 1543698085383
newdate=Date(newdate); //3
console.log(typeof newdate, newdate); //string Wed Dec 12 2018 23:04:44 GMT+0200 (Eastern European Standard Time)
var d = newdate.getDate(); // 4
console.log(typeof d, d); //
Date type is assigned to the object.
number is assigned to newdate. which is ticks
returns string
string.getDate() is not defined, so undefined.
hope it helps.

Daylight javascript formatting date

I have this problem.
I have this date with this format
var datestring = "2017-10-30T15:03:10.933044Z";
If I write my code like this
var d = new Date(datestring);
I obtaine
Mon Oct 30 2017 16:03:10 GMT+0100 (ora solare Europa occidentale)
because there is one hour of a daylight in italy now. Nevertheless, I would like to have the same hour of 'datestring' (15, and not 16).
Could you help me?
thank you very much
According to ECMA-262, if you want to treat an ISO 8601 format UTC timestamp as local, just remove the Z. However, it will now represent a different moment in time if the local timezone is not GMT+0000.
Also, using the built-in parser is not recommended (see Why does Date.parse give incorrect results?), as some browsers will still treat it as UTC (e.g. Safari 11) or perhaps invalid. You should either write your own function to parse the string, or use a library. There are plenty of good parsing and formatting libraries available.
var s = '2017-10-30T15:03:10.933044Z';
var d = new Date(s.replace(/z/i,''));
console.log(d.toString());
Your input string is in ISO-8601 format. In this format, the Z at the end means the timestamp is UTC-based.
You can obtain a more human-friendly UTC-based string representation with the .toUTCString() method.
var datestring = "2017-10-30T15:03:10.933044Z";
var d = new Date(datestring);
var s = d.toUTCString();
console.log(s) // "Mon, 30 Oct 2017 15:03:10 GMT"
If you want the string in a specific format, then consider using a library like Moment.js.

javascript Date timezone issue

I need a js Date object with specified values for date and year. I would expect
new Date("2000-01-01") to give me Date object with 2000 as value for getFullYear(), but if my computer's time settings are set to Chicago timezone, I'm getting Fri Dec 31 1999 18:00:00 GMT-0600 (CST), and for Buenos Aires: Fri Dec 31 1999 22:00:00 GMT-0200 (ARST).
Is there a way to create Date object, with .getFullYear() returning the date we set in constructor, no matter what timezone is set on user's machine?
Update:
I need this Date object to be used in another library (which calls its .getFullYear() method, so using UTC getters doesn't really help.
When parsing a string to a Date in JavaScript, a value that is in YYYY-MM-DD format is interpreted as a UTC value, rather than a local-time value.
The key is that the parts are separated by hyphens, and that there is no time zone information in the string. The ECMAScript 5.1 Spec says in §15.9.1.15:
... The value of an absent time zone offset is “Z”.
That means, if you don't specify an offset, it will assume you meant UTC.
Note that since this is the opposite of what ISO-8601 says, this is behavior has been changed in ECMAScript 2015 (6.0), which says in §20.3.1.16:
... If the time zone offset is absent, the date-time is interpreted as a local time.
Therefore, when this provision of ES6 is implemented properly, string values of this format that used to be interpreted as UTC will be interpreted as local time instead. I've blogged about this here.
The workaround is simple. Replace the hyphens with slashes:
var s = "2000-01-01";
var dt = new Date(s.replace(/-/g, '/'));
Another workaround that is acceptable is to assign a time of noon instead of midnight to the date. This will be parsed as local time, and is far enough away to avoid any DST conflicts.
var s = "2000-01-01";
var dt = new Date(s + "T12:00:00");
Alternatively, consider a library like moment.js which is much more sensible.
var s = "2000-01-01";
var dt = moment(s, 'YYYY-MM-DD').toDate();
You can write new method to 'Date.prototype', and use it to get date which will be including the local timezone offset.
//return the date with depend of Local Time Zone
Date.prototype.getUTCLocalDate = function () {
var target = new Date(this.valueOf());
var offset = target.getTimezoneOffset();
var Y = target.getUTCFullYear();
var M = target.getUTCMonth();
var D = target.getUTCDate();
var h = target.getUTCHours();
var m = target.getUTCMinutes();
var s = target.getUTCSeconds();
return new Date(Date.UTC(Y, M, D, h, m + offset, s));
};
Here is a little trick that may help someone:
let date = new Date();
console.log(date); // -> Fri May 28 2021 01:04:26 GMT+0200 (Central European Summer Time)
const tzOffsetMin = Math.abs(date.getTimezoneOffset()) // the minutes of the offset timezone
const tzOffsetHour = tzOffsetMin / 60; // timezone offset in hour
console.log(tzOffsetHour); // -> 2
date.setHours(date.getHours() + tzOffsetHour); // sum to date hour the timezoneoffset
const isovalue = date.toISOString();
console.log(isovalue); // -> 2021-05-28T01:04:26.156Z
In this way you "bypass" the timezone offset wherever you are

Why is Date("2014-04-07") parsed to Sat Apr 05 2014 17:26:15 GMT-0500 (CEST)?

I'm creating dates like this:
var StartDate = new Date(data.feed.entry[i].gd$when[j].startTime);
When a date string is received specifying date and time in the form:
"2014-04-12T20:00:00.000-05:00"
Date() interprets this perfectly fine returning:
Sat Apr 12 2014 19:00:00 GMT-0500 (CDT)
However, when the date string is received with no time information in the form:
"2014-04-07"
then Date() is interpreting it as:
Sat Apr 05 2014 19:00:00 GMT-0500 (CDT)
Looks like Date() is taking the -07 as the time and I have no clue where is it getting the date as 05. Any idea what might be the problem?
Could it be, somehow, Date() is interpreting a different time zone because in the first string the time zone is determined at the very end but in the "all day" event there is no indication of the time zone.
Has anybody found this issue? If yes, how did you solve it?
UPDATE: After researching a little bit more this parsing issue I noticed something very weird:
The following statement:
new Date("2014-4-07")
would return Mon Apr 07 2014 00:00:00 GMT-0500 (CDT) which is correct, but the following one:
new Date("2014-04-07")
returns Sun Apr 06 2014 19:00:00 GMT-0500 (CDT) which is the wrong one. So, for whatever reason, seems like the padding zeros affect the way the date is parsed!
You're using the Date() function wrong.
It only accepts parameters in the following formats.
//No parameters
var today = new Date();
//Date and time, no time-zone
var birthday = new Date("December 17, 1995 03:24:00");
//Date and time, no time-zone
var birthday = new Date("1995-12-17T03:24:00");
//Only date as integer values
var birthday = new Date(1995,11,17);
//Date and time as integer values, no time-zone
var birthday = new Date(1995,11,17,3,24,0);
Source: MDN.
The Date() function does not accept timezone as a parameter. The reason why you think the time-zone parameter works is because its showing the same time-zone that you entered, but that's because you're in the same time-zone.
The reason why you get Sat Apr 05 2014 19:00:00 GMT-0500 (CDT) as your output for Date("2014-04-07" ) is simply because you used it in a different way.
new Date(parameters) will give the output according to the parameters passed in it.
Date(parameters) will give the output as the current date and time no matter what parameter you pass in it.
Prior to ES5, parsing of date strings was entirely implementation dependent. ES5 specifies a version of ISO 8601 that is supported by may browsers, but not all. The specified format only supports the Z timezone (UTC) and assumes UTC if the timezone is missing. Support where the timezone is missing is inconsistent, some implementations will treat the string as UTC and some as local.
To be certain, you should parse the string yourself, e.g.
/* Parse an ISO string with or without an offset
** e.g. '2014-04-02T20:00:00-0600'
** '2014-04-02T20:00:00Z'
**
** Allows decimal seconds if supplied
** e.g. '2014-04-02T20:00:00.123-0600'
**
** If no offset is supplied (or it's Z), treat as UTC (per ECMA-262)
**
** If date only, e.g. '2014-04-02', treat as UTC date (per ECMA-262)
*/
function parseISOString(s) {
var t = s.split(/\D+/g);
var hasOffset = /\d{2}[-+]\d{4}$/.test(s);
// Whether decimal seconds are present changes the offset field and ms value
var hasDecimalSeconds = /T\d{2}:\d{2}:\d{2}\.\d+/i.test(s);
var offset = hasDecimalSeconds? t[7] : t[6];
var ms = hasDecimalSeconds? t[6] : 0;
var offMin, offSign, min;
// If there's an offset, apply it to minutes to get a UTC time value
if (hasOffset) {
offMin = 60 * offset / 100 + offset % 100;
offSign = /-\d{4}$/.test(s)? -1 : 1;
}
min = hasOffset? +t[4] - offMin * offSign : (t[4] || 0);
// Return a date object based on UTC values
return new Date(Date.UTC(t[0], --t[1], t[2], t[3]||0, min, t[5]||0, ms));
}
An ISO 8601 date string should be treated as UTC (per ECMA-262), so if you are UTC-0500, then:
new Date('2014-04-07'); // 2014-04-06T19:00:00-0500
The behaviour described in the OP shows the host is not compliant with ECMA-262. Further encouragement to parse the string yourself. If you want the date to be treated as local, then:
// Expect string in ISO 8601 format
// Offset is ignored, Date is created as local time
function parseLocalISODate(s) {
s = s.split(/\D+/g);
return new Date(s[0], --s[1], s[2],0,0,0,0);
}
In your function you can do something like:
var ds = data.feed.entry[i].gd$when[j].startTime;
var startDate = ds.length == 10? parseLocalISODate(ds) : parseISOString(ds);
Also note that variables starting with a capital letter are, by convention, reserved for constructors, hence startDate, not StartDate.
(I would add a comment but i don't have 50 rep yet)
See what
new Date().getTimezoneOffset()
returns, I would expect a big negative value, that would be the only reasonable explanation to your problem.
I have had some trouble with date conversions in the past, in particular with daytime saving timezones, and as work around i always set the time explicitly to midday (12:00am). Since I think you were using knockout, you could just make a computed observable that appends a "T20:00:00.000-05:00" or the appropiate time zone to all "day only" dates

Parse date without timezone javascript

I want to parse a date without a timezone in JavaScript. I tried:
new Date(Date.parse("2005-07-08T00:00:00+0000"));
Which returned Fri Jul 08 2005 02:00:00 GMT+0200 (Central European Daylight Time):
new Date(Date.parse("2005-07-08 00:00:00 GMT+0000"));
returns the same result and:
new Date(Date.parse("2005-07-08 00:00:00 GMT-0000"));
also returns the same result.
I want to parse time:
without time zone.
without calling a constructor Date.UTC or new Date(year, month, day).
by simply passing a string into the Date constructor (without prototype approaches).
I have to produce a Date object, not a String.
I have the same issue. I get a date as a String, for example: '2016-08-25T00:00:00', but I need to have Date object with correct time. To convert String into object, I use getTimezoneOffset:
var date = new Date('2016-08-25T00:00:00')
var userTimezoneOffset = date.getTimezoneOffset() * 60000;
new Date(date.getTime() - userTimezoneOffset);
getTimezoneOffset() will return ether negative or positive value. This must be subtracted to work in every location in world.
The date is parsed correctly, it's just toString that converts it to your local timezone:
let s = "2005-07-08T11:22:33+0000";
let d = new Date(Date.parse(s));
// this logs for me
// "Fri Jul 08 2005 13:22:33 GMT+0200 (Central European Summer Time)"
// and something else for you
console.log(d.toString())
// this logs
// Fri, 08 Jul 2005 11:22:33 GMT
// for everyone
console.log(d.toUTCString())
Javascript Date object are timestamps - they merely contain a number of milliseconds since the epoch. There is no timezone info in a Date object. Which calendar date (day, minutes, seconds) this timestamp represents is a matter of the interpretation (one of to...String methods).
The above example shows that the date is being parsed correctly - that is, it actually contains an amount of milliseconds corresponding to "2005-07-08T11:22:33" in GMT.
I ran into the same problem and then remembered something wonky about a legacy project I was working on and how they handled this issue. I didn't understand it at the time and didn't really care until I ran into the problem myself
var date = '2014-01-02T00:00:00.000Z'
date = date.substring(0,10).split('-')
date = date[1] + '-' + date[2] + '-' + date[0]
new Date(date) #Thu Jan 02 2014 00:00:00 GMT-0600
For whatever reason passing the date in as "01-02-2014" sets the timezone to zero and ignores the user's timezone. This may be a fluke in the Date class but it existed some time ago and exists today. And it seems to work cross-browser. Try it for yourself.
This code is implemented in a global project where timezones matter a lot but the person looking at the date did not care about the exact moment it was introduced.
I found JavaScript Date Object and Time Zones | Fixing an "off by 1 day" bug on YouTube. This fixes/resets the offset for the local timezone. There's a great explanation to this problem in the video.
// date as YYYY-MM-DDT00:00:00Z
let dateFormat = new Date(date)
// Methods on Date Object will convert from UTC to users timezone
// Set minutes to current minutes (UTC) + User local time UTC offset
dateFormat.setMinutes(dateFormat.getMinutes() + dateFormat.getTimezoneOffset())
// Now we can use methods on the date obj without the timezone conversion
let dateStr = dateFormat.toDateString();
Since it is really a formatting issue when displaying the date (e.g. displays in local time), I like to use the new(ish) Intl.DateTimeFormat object to perform the formatting as it is more explicit and provides more output options:
const dateOptions = { timeZone: 'UTC', month: 'long', day: 'numeric', year: 'numeric' };
const dateFormatter = new Intl.DateTimeFormat('en-US', dateOptions);
const dateAsFormattedString = dateFormatter.format(new Date('2019-06-01T00:00:00.000+00:00'));
console.log(dateAsFormattedString) // "June 1, 2019"
As shown, by setting the timeZone to 'UTC' it will not perform local conversions. As a bonus, it also allows you to create more polished outputs. You can read more about the Intl.DateTimeFormat object in Mozilla - Intl.DateTimeFormat.
The same functionality can be achieved without creating a new Intl.DateTimeFormat object. Simply pass the locale and date options directly into the toLocaleDateString() function.
const dateOptions = { timeZone: 'UTC', month: 'long', day: 'numeric', year: 'numeric' };
const myDate = new Date('2019-06-01T00:00:00.000+00:00');
myDate.toLocaleDateString('en-US', dateOptions); // "June 1, 2019"
The Date object itself will contain timezone anyway, and the returned result is the effect of converting it to string in a default way. I.e. you cannot create a date object without timezone. But what you can do is mimic the behavior of Date object by creating your own one.
This is, however, better to be handed over to libraries like moment.js.
Date in JavaScript is just keeping it simple inside, so the date-time data is stored in UTC Unix epoch (milliseconds or ms).
If you want to have a "fixed" time that doesn't change in whatever timezone you are on the earth, you can adjust the time in UTC to match your current local timezone and save it. And when retrieving it, in whatever your local timezone you are in, it will show the adjusted UTC time based on the one who saved it and then add the local timezone offset to get the "fixed" time.
To save date (in ms):
toUTC(datetime) {
const myDate = (typeof datetime === 'number')
? new Date(datetime)
: datetime;
if (!myDate || (typeof myDate.getTime !== 'function')) {
return 0;
}
const getUTC = myDate.getTime();
const offset = myDate.getTimezoneOffset() * 60000; // It's in minutes so convert to ms
return getUTC - offset; // UTC - OFFSET
}
To retrieve/show date (in ms):
fromUTC(datetime) {
const myDate = (typeof datetime === 'number')
? new Date(datetime)
: datetime;
if (!myDate || (typeof myDate.getTime !== 'function')) {
return 0;
}
const getUTC = myDate.getTime();
const offset = myDate.getTimezoneOffset() * 60000; // It's in minutes so convert to ms
return getUTC + offset; // UTC + OFFSET
}
Then you can:
const saveTime = new Date(toUTC(Date.parse("2005-07-08T00:00:00+0000")));
// SEND TO DB....
// FROM DB...
const showTime = new Date(fromUTC(saveTime));
You can use this code
var stringDate = "2005-07-08T00:00:00+0000";
var dTimezone = new Date();
var offset = dTimezone.getTimezoneOffset() / 60;
var date = new Date(Date.parse(stringDate));
date.setHours(date.getHours() + offset);
Here's a simple solution:
const handler1 = {
construct(target, args) {
let newDate = new target(...args);
var tzDifference = newDate.getTimezoneOffset();
return new target(newDate.getTime() + tzDifference * 60 * 1000);
}
};
Date = new Proxy(Date, handler1);
The solution is almost the same as #wawka's, however it handles different timezones with plus and minus sings using Math.abs:
const date = new Date("2021-05-24T22:00:18.512Z")
const userTimezoneOffset = Math.abs(date.getTimezoneOffset() * 60000);
new Date(date.getTime() - userTimezoneOffset);
The only time new Date() does the time zone conversion is when you pass the time zone reference. For e.g. in the following string "2022-08-16T10:54:12Z" the Z at the end is a reference to timezone. If the object is passing this variable at the end, you can use the following code to get a new date object without time conversion:
const dateStr = '2022-07-21T09:35:31.820Z';
const date = new Date(dateStr);
console.log(date); // 👉️ Thu Jul 21 2022 12:35:31 GMT+0300
const result = new Date(date.toISOString().slice(0, -1));
console.log(result); // 👉️ Thu Jul 21 2022 09:35:31 GMT+0300
I would personally prefer #wawka's answer, however, I also came up with a not so clean trick to solve this problem, which is simpler and can work if you are sure about the format of the strings you want to convert.
Look at the code snippet below:
var dateString = '2021-08-02T00:00:00'
var dateObj = new Date(dateString + 'Z')
console.log("No Timezone manipulation: ", dateObj)
var dateObjWithTZ = new Date(dateString)
console.log("Normal conversion: ", dateObjWithTZ)
This works in this case, because adding a Z to the end of the date time string will make JS treat this string to be a UTC date string, so it does not add timezone difference to it.
Timezone is a part of Javascript. I used the following code to adjust the date according to the timezone.
var dt = new Date("Fri Mar 11, 2022 4:03 PM");
dt.setTime(dt.getTime() - dt.getTimezoneOffset() *60 * 1000); //Adjust for Timezone
document.write(dt.toISOString());
This is the solution that I came up with for this problem which works for me.
library used: momentjs with plain javascript Date class.
Step 1.
Convert String date to moment object (PS: moment retains the original date and time as long as toDate() method is not called):
const dateMoment = moment("2005-07-08T11:22:33+0000");
Step 2.
Extract hours and minutes values from the previously created moment object:
const hours = dateMoment.hours();
const mins = dateMoment.minutes();
Step 3.
Convert moment to Date(PS: this will change the original date based on the timezone of your browser/machine, but don't worry and read step 4.):
const dateObj = dateMoment.toDate();
Step 4.
Manually set the hours and minutes extracted in Step 2.
dateObj.setHours(hours);
dateObj.setMinutes(mins);
Step 5.
dateObj will now have show the original Date without any timezone difference. Even the Daylight time changes won't have any effect on the date object as we are manually setting the original hours and minutes.
Hope this helps.
(new Date().toString()).replace(/ \w+-\d+ \(.*\)$/,"")
This will have output: Tue Jul 10 2018 19:07:11
(new Date("2005-07-08T11:22:33+0000").toString()).replace(/ \w+-\d+ \(.*\)$/,"")
This will have output: Fri Jul 08 2005 04:22:33
Note: The time returned will depend on your local timezone
There are some inherent problems with date parsing that are unfortunately not addressed well by default.
-Human readable dates have implicit timezone in them
-There are many widely used date formats around the web that are ambiguous
To solve these problems easy and clean one would need a function like this:
>parse(whateverDateTimeString,expectedDatePattern,timezone)
"unix time in milliseconds"
I have searched for this, but found nothing like that!
So I created:
https://github.com/zsoltszabo/timestamp-grabber
Enjoy!

Categories

Resources