Engineering

Building a Smart Datetime Picker – without using AI

We recently built a smart datetime picker that lets you enter any arbitrary time interval in natural language and auto-magically converts it to the right datetime format. Here's how we built it.

Building a Smart Datetime Picker – without using AI

We recently gave our link expiration feature a facelift.

Dub's link expiration feature allows you to set an expiration date for your short links. After the expiration date, the shared link will no longer be accessible. Optionally, you can also set a custom expiration URL to send users to after the link expires.

One of the key improvements was the introduction of a smart datetime picker – an input that lets you enter any arbitrary time interval in natural language and auto-magically converts it to the right datetime format.

For example, you can type "next Friday at 3pm" or "in 2 weeks" and the datetime picker will convert it to the correct date and time format – instantaneously.

When we shared this on X (formerly Twitter), the response was overwhelmingly positive. Many folks were impressed by how intuitive this feature was and some even thought we were using AI behind the scenes.

In this post, we'll share how we built this smart datetime picker – without using AI.

Live Demo

Before we dive into the details, here's a live demo of the smart datetime picker in action – feel free to take it for a spin! And if you do find any bugs, please let us know.

The secret sauce: chrono-node

The smart datetime picker is actually quite simple under the hood. Most of the heavy-lifting is done by a library called Chrono, which is a natural language date parser in JavaScript.

With this library, you can parse a wide range of date and time formats, such as:

  • "next Friday at 3pm"
  • "in 2 weeks"
  • "tomorrow"
  • "2 days from now"
  • Sat Aug 17 2013 18:40:39 GMT+0900 (JST)
  • 2014-11-30T08:15:30-05:30

It also supports multiple languages, so you can parse dates in different languages as well.

The current fully supported languages in chrono-node are en, ja, fr, nl, ru and uk (de, pt, and zh.hant are partially supported).

Creating utility functions for date parsing and formatting

With the chrono-node library, we created a simple utility function that would take in a date string and return a Date object:

parse-datetime.ts
import * as chrono from "chrono-node";
 
// Function to parse a date string into a Date object
export const parseDateTime = (str: Date | string) => {
  if (str instanceof Date) return str;
  return chrono.parseDate(str);
};

We also created a formatDateTime function that would take in a Date object and return a formatted string in the following format: MMM d, yyyy, h:mm a (e.g. Apr 8, 2024, 3:00 PM).

format-datetime.ts
export const formatDateTime = (datetime: Date | string) => {
  return new Date(datetime).toLocaleTimeString("en-US", {
    month: "short",
    day: "numeric",
    year: "numeric",
    hour: "numeric",
    minute: "numeric",
    hour12: true,
  });
};

As pointed out by some folks on X, a potential enhancement here is to set the date format in the user's locale. E.g. in the UK, the date format is typically d MMM yyyy (e.g. 8 Apr 2024).

Last but not least, we created a getDateTimeLocal function that would return the current date and time in the correct timezone format for the datetime-local input field:

get-datetime-local.ts
export const getDateTimeLocal = (timestamp?: Date): string => {
  const d = timestamp ? new Date(timestamp) : new Date();
  if (d.toString() === "Invalid Date") return "";
  return new Date(d.getTime() - d.getTimezoneOffset() * 60000)
    .toISOString()
    .split(":")
    .slice(0, 2)
    .join(":");
};

With these utility functions in place, we can now start building the smart datetime picker.

Building the smart datetime picker

The smart datetime picker is a simple React component that consists of two input fields:

  1. A text input field for entering the date and time in natural language
  2. A datetime-local input field as a fallback for selecting the date and time manually using the browser's native date and time picker

Part 1: Natural Language Input

The text input field is where the magic happens. When you enter a natural language date and time, the onBlur event handler will parse the date string using the parseDateTime function and update the expiresAt state with the parsed Date object.

natural-language-input.tsx
// import the utility functions that we created above
import { formatDateTime, parseDateTime } from "@dub/utils";
 
function NaturalLanguageInput({ expiresAt, setExpiresAt }) {
  const inputRef = useRef<HTMLInputElement>(null);
 
  return (
    <input
        ref={inputRef}
        type="text"
        placeholder='E.g. "tomorrow at 5pm" or "in 2 hours"'
        defaultValue={expiresAt ? formatDateTime(expiresAt) : ""}
        onBlur={(e) => { // parse the date string when the input field loses focus
        if (e.target.value.length > 0) {
            const parsedDateTime = parseDateTime(e.target.value);
            if (parsedDateTime) {
            setExpiresAt(parsedDateTime);
            e.target.value = formatDateTime(parsedDateTime);
            }
        }
        }}
        className="flex-1 border-none bg-transparent text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-0 sm:text-sm"
    />
  );
}

Part 2: DateTime Local Input

The datetime-local input field is used as a fallback in case the user prefers to enter the date and time manually using the browser's native date and time picker. The onChange event handler will update the expiresAt state with the parsed Date object to keep the two input fields in sync.

datetime-local-input.tsx
// import the utility functions that we created above
import { formatDateTime, getDateTimeLocal } from "@dub/utils";
 
function DateTimeLocalInput({ expiresAt, setExpiresAt }) {
  return (
    <input
        type="datetime-local"
        id="expiresAt"
        name="expiresAt"
        value={expiresAt ? getDateTimeLocal(expiresAt) : ""}
        onChange={(e) => {
        const expiryDate = new Date(e.target.value);
        setExpiresAt(expiryDate);
        // set the formatted date string in the text input field to keep them in sync
        if (inputRef.current) {
            inputRef.current.value = formatDateTime(expiryDate);
        }
        }}
        // we intentionally make the datetime-local input field with a width of 40px
        // to only show the calendar icon and hide the input field
        className="w-[40px] border-none bg-transparent text-gray-500 focus:outline-none focus:ring-0 sm:text-sm"
    />
  );
}

Putting it all together

To put it all together, we can create a parent component that renders both the NaturalLanguageInput and DateTimeLocalInput components side by side. The parent component will manage the expiresAt state and pass it down to the child components.

smart-datetime-picker.tsx
import { useRef, useState } from "react";
import { NaturalLanguageInput } from "./natural-language-input";
import { DateTimeLocalInput } from "./datetime-local-input";
 
export default function SmartDatetimePicker() {
  const inputRef = useRef<HTMLInputElement>(null);
  const [expiresAt, setExpiresAt] = useState<Date | null>(null);
 
  return (
    <div className="flex items-center justify-center p-8">
      <div className="flex w-full max-w-sm items-center justify-between rounded-md border border-gray-300 bg-white shadow-sm transition-all focus-within:border-gray-800 focus-within:outline-none focus-within:ring-1 focus-within:ring-gray-500">
        <NaturalLanguageInput expiresAt={expiresAt} setExpiresAt={setExpiresAt} />
        <DateTimeLocalInput expiresAt={expiresAt} setExpiresAt={setExpiresAt} />
      </div>
    </div>
  );
}

Optionally, you can also colocate the React Components all in a single file for simplicity, which is how we're doing it in our codebase.

Bonus: Adding smart datetime parsing to the Dub API

We also added this smart datetime parsing logic to the Dub API, so you can pass the following JSON payload to create a short link that expires in 2 weeks:

{
  "url": "https://dub.co/blog/smart-datetime-picker",
  "expiresAt": "in 2 weeks" // also works with the usual "2024-04-22T12:00:00Z"
}

Check out the Dub API documentation to learn more about how you can programmatically create short links with our link infrastructure.

Conclusion

And that's it! With just a few utility functions and a couple of React components, you can build a smart datetime picker that lets users enter natural language date and time intervals.

We hope you found this post helpful for implementing a similar feature in your own projects. If you have any questions or feedback, feel free to reach out to us on X.

Our codebase is fully open-source, so feel free to check it out and learn more about how we implemented the smart datetime picker.

Supercharge your marketing efforts

See why Dub is the link management infrastructure of choice for modern marketing teams.