So, I'm currently working on an application that's very data-driven. When a user signs up they're given a default state within the application for the current, previous, and next month from when the user signs up. Now, I'm creating this lesson on October 31st, which gives me a unique opportunity to test my application using a real-world scenario and I thought I'd drag you along with me to walk through what I'm doing to handle user-specific dates in my application.
Desired Functionality
So, when a user signs up what I want my application to do for users currently in my timezone, which is ETC where it's October 31st, is to create a default state for the months October, September, and November. However, for users in Eastern Asia and Australia where it's already November 1st, I'd like my application to create a default state for the months November, October, and December. This is my desired behavior when a user signs up, which from there I'd have a cron job set up to take care of all future months.
The Problem
Given this scenario, if we were to just use Luxon's DateTime.now()
throughout our backend, on sign up, that would only create the default state for whatever day it is for the server, which I always default to UTC. So, for my application, that's very limiting. In my case, I'd need to know what the specific timezone is for the user.
The Solution
My application consists of a backend API using AdonisJS and a frontend SPA using VueJS that are decoupled from one another. So, what I really want to have happen here is for my frontend to inform my backend what my user's timezone is. Then, I need my backend to create time-sensitive dates with that timezone in mind.
Frontend
So, in order to inform my backend what timezone my user is in, I'm adding a global header onto my Axios configuration called Timezone
and I'm setting this value using Intl.DateTimeFormat().resolvedOptions().timeZone
.
axios.defaults.headers.common['Timezone'] = Intl.DateTimeFormat().resolvedOptions().timeZone
So, with me being in Eastern Standard Time (EST), this would set time Timezone
header to America/New_York
. That's not where I live, but it is the identifier for my time zone.
Backend
Then, in my backend, I have created a PluckTimezone
middleware that plucks the user's timezone off the request's header. Then, creates a Luxon DateTime object set to that user's timezone. I'm then placing both the user's timezone and its DateTime object onto the request's HttpContext.
// app/Middleware/PluckTimezone.ts
import { DateTime } from 'luxon'
export default class PluckTimezone {
public async handle (ctx: HttpContextContract, next: () => Promise<void>) {
const timezone = ctx.request.header('Timezone', 'UTC')
const dateTime = DateTime.now().setZone(timezone)
ctx.userTimezone = timezone
ctx.userDateTime = dateTime
// code for middleware goes here. ABOVE THE NEXT CALL
await next()
}
}
Then, anywhere I need to create, update, or query time-sensitive data, I can make use of the userDateTime
off my HttpContext.
So, for example, I have a route within this application defined as the below where it optionally takes a year and a month. If neither is provided it will default to whatever the user's current month and year are.
// start/routes.ts
Route.get('budget/:year?/:month?', 'BudgetController.index')
.where('year', Route.matchers.number())
.where('month', Route.matchers.number())
This route's controller then looks like the below.
// app/Controllers/Http/BudgetController.ts
import { inject } from '@adonisjs/fold'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import BudgetService from 'App/Services/BudgetService'
@inject()
export default class BudgetController {
constructor(public budgetService: BudgetService) {}
public async index ({ response, params, userDateTime }: HttpContextContract) {
const year = params.year ?? userDateTime.year
const month = params.month ?? userDateTime.month
const budgetDetails = await this.budgetService.getBudgetDetails(year, month)
return response.status(200).json({
year,
month,
budgetDetails
})
}
}
Here I'm attempting to grab the year and month off my route parameters. However, if those values aren't there the year and month will fall back to using the userDateTime
off my HttpContext where the year and month would be set to whatever the year and month currently are for the user.
If we take a look at another example, here we're within a service that creates the user's default budget for the current, previous, and next month. Note, within this service, my HttpContext is tied to my service instance as ctx
.
// app/Services/BudgetService.ts > setDefaultBudget()
/**
* Create default budget for new user. Creates budget for current, previous, and next month.
* @param subAccount
* @returns
*/
public async setDefaultBudget(subAccount: SubAccount) {
await this.createInflowGroupCategory(subAccount)
const date = this.ctx.userDateTime
const budget = await this.createDefaultBudget(subAccount, date)
// create budget for previous and next month
await BudgetSyncService.copyUserBudgetForNewMonth(subAccount, date, date.minus({ months: 1 }))
await BudgetSyncService.copyUserBudgetForNewMonth(subAccount, date, date.plus({ months: 1 }))
return budget
}
Here I'm grabbing the user's DateTime off my HttpContext via this.ctx.userDateTime
. Then, I'm providing that date to the method that creates the default budget for the current month.
Then, to create the default budget for the previous and next months, I'm using DateTime's minus
and plus
methods to add and remove a month. Since Luxon's DateTime is immutable, it won't mutate the userDateTime
on my HttpContext, but instead, return a new instance of the object with the change made.
Wrapping Up
I felt like, with me both working on this functionality and it being the last day of the month (at the time of putting this lesson together), I had a unique opportunity to cover this topic. It's a bit harder to do written than it is walking through it in a video, but hopefully, you were able to follow along okay. If you need visuals, please do also check out the video. I visibly show how using the server's time and the user's time impacts the application.
Join The Discussion! (0 Comments)
Please sign in or sign up for free to join in on the dicussion.
Be the first to Comment!