Watch our biggest AI launch event

Announcing Visual Copilot - Figma to production in half the time

Builder.io logo
Contact Sales
Contact Sales

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

Web Development

How we built the dynamic tickets feature for Builder Accelerate

March 6, 2024

Written By Vishwas Gopinath

On March 13th, we will host Builder Accelerate, an AI launch event focused on leveraging AI to convert designs into code with your design system. As part of the registration experience, we’ve introduced a feature which allows the generation of a personalized event ticket based on the user's domain. For example, twitch.com will produce a ticket infused with Twitch's brand colors:

In this blog post, I’ll break down this feature using React for the frontend and Express for the backend (though it was originally built with Qwik). We will focus primarily on the concept without diving too deeply into the specifics of frameworks being used. You can find the source code on my GitHub repo.

If you're someone who learns best through visual content, check out the video tutorial.

Setting up your project

We'll start by setting up a React application using Vite and a Node.js Express server. The goal is to create an API endpoint in Express that returns necessary ticket information based on a given domain, focusing primarily on brand colors.

First, let's dive into the Express server setup. We'll use the express and cors packages to create a basic server:

import express from "express";
import cors from "cors";

const app = express();
app.use(cors());

app.get("/", (_, res) => {
  res.send("Hello World");
});

app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

Our server runs on port 3000, and visiting localhost:3000 displays a "Hello World" message.

We introduce a /ticket route that takes a domain as a query parameter. We then construct the URL to fetch the favicon associated with the domain. Google provides a convenient URL format to retrieve a site's favicon:

app.get("/ticket", async (req, res) => {
  const domain = req.query.domain;
  const faviconUrl = `https://www.google.com/s2/favicons?domain=${domain}&sz=128`;
});

Using the colorthief package, we extract the primary color and a palette of colors from the favicon:

const [primaryColorRGB, paletteRGB] = await Promise.all([
  colorthief.getColor(faviconUrl),
  colorthief.getPalette(faviconUrl),
]);

Here, primaryColorRGB holds the RGB values of the dominant color, and paletteRGB contains an array of colors representing the favicon's color palette.

The secondary color is chosen to provide a good contrast with the primary color.

Define a function to calculate the distance between two colors in RGB space. This helps in finding a color in the palette that is most dissimilar to the primary color:

function rgbDistance(color1, color2) {
  let rDiff = Math.pow(color1[0] - color2[0], 2);
  let gDiff = Math.pow(color1[1] - color2[1], 2);
  let bDiff = Math.pow(color1[2] - color2[2], 2);
  return Math.sqrt(rDiff + gDiff + bDiff);
}

Iterate through the palette to find the color that is furthest away from the primary color:

function findDissimilarColor(primaryColor, colorPalette) {
  let maxDistance = -1;
  let secondaryColor = null;

  colorPalette.forEach((color) => {
    let distance = rgbDistance(primaryColor, color);
    if (distance > maxDistance) {
      maxDistance = distance;
      secondaryColor = color;
    }
  });

  return secondaryColor;
}

Call this function with the primary color and the palette to determine the secondary color:

  const secondaryColorRGB = findDissimilarColor(primaryColorRGB, paletteRGB);

To ensure the text on the ticket is readable regardless of the background color, we need to determine whether the color is dark or light. Implement a function to check the luminance of a color:

1. Calculate relative luminance: The luminance of a color is a measure of the intensity of the light that it emits. Use the following function to calculate it:

function isDarkColor(color) {
  let luminance =
    0.2126 * (color[0] / 255) +
    0.7152 * (color[1] / 255) +
    0.0722 * (color[2] / 255);
  return luminance < 0.5;
}

2. Apply the function: Use this function to determine if the primary and secondary colors are dark or light. This information is crucial for setting the text color on the ticket for optimal readability:

const isPrimaryColorDark = isDarkColor(primaryColorRGB);
const isSecondaryColorDark = isDarkColor(secondaryColorRGB);

Now that all the ticket information has been calculated, return the domain, the favicon URL, the primary color in both RGB and hex formats, the secondary color in both RGB and hex formats, and whether the two colors are dark.

res.json({
  domain,
  faviconUrl,
  primaryColorRGB,
  primaryColorHex: rgbToHex(...primaryColorRGB),
  isPrimaryColorDark: isDarkColor(primaryColorRGB),
  secondaryColorRGB,
  secondaryColorHex: rgbToHex(...secondaryColorRGB),
  isSecondaryColorDark: isDarkColor(secondaryColorRGB),
});

If you navigate to localhost:3000/ticket?domain=builder.io, you should see the API returning the ticket information.

In the React frontend, create an input field to accept the domain and a button to generate the ticket. The frontend communicates with the Express backend to fetch and display the ticket information. I’ve taken the good-enough-is-fine approach to writing the React code. Here’s the most basic code to get it working:

// App.jsx

import { useState } from "react";
import { Ticket } from "./Ticket";

export default function App() {
  const [domain, setDomain] = useState("builder.io");
  const [ticketInfo, setTicketInfo] = useState({});

  const fetchTicketInfo = async () => {
    const response = await fetch(
      `http://localhost:3000/ticket?domain=${domain}`
    );
    const data = await response.json();
    setTicketInfo(data);
  };

  return (
    <div className="bg-black h-screen flex flex-col justify-center items-center">
      <input
        type="text"
        className="p-2 rounded mb-4"
        value={domain}
        onChange={(e) => setDomain(e.target.value)}
        onKeyDown={(e) => e.key === "Enter" && fetchTicketInfo()}
      />
      {!!ticketInfo.faviconUrl && <Ticket ticketInfo={ticketInfo} />}
    </div>
  );
}

Create a Ticket component in React to display the ticket and use the fetched color data to dynamically style the ticket based on the input domain's brand colors:

// Ticket.jsx

import { BuilderTicketLogo } from "./assets/BuilderLogo";

const padStartWithZero = (num = 0) => {
  return num.toString().padStart(7, "0");
};

export const Ticket = (props) => {
  const primaryColor = props.ticketInfo.primaryColorHex;
  const secondaryColor = props.ticketInfo.secondaryColorHex;
  const isPrimaryColorDark = props.ticketInfo.isPrimaryColorDark;
  const isSecondaryColorDark = props.ticketInfo.isSecondaryColorDark;
  const favicon = props.ticketInfo.faviconUrl;
  const companyName = props.ticketInfo.domain;
  const ticketNo = padStartWithZero("12345");

  return (
    <div className="w-[600px]">
      <div className="flex">
        <div
          id="left"
          style={{
            backgroundColor: primaryColor,
            color: isPrimaryColorDark ? "white" : "black",
          }}
          className="rounded-l-lg border-r-0 pt-8 pb-4"
        >
          <div className="flex justify-between items-center px-12 mb-4">
            <BuilderTicketLogo isDark={isPrimaryColorDark} />
            <span
              style={{
                color: isPrimaryColorDark ? "white" : "black",
              }}
              className="text-sm"
            >
              An AI launch event <strong>accelerate.builder.io</strong>
            </span>
          </div>
          <div
            style={{
              color: isPrimaryColorDark ? "white" : "black",
            }}
            className="text-6xl px-12 mb-2"
          >
            Accelerate &gt;&gt;&gt;
          </div>
          <div className="flex items-center px-12 mb-4">
            <span className="text-lg">March 13, 2024</span>
            <span
              style={{
                color: secondaryColor,
              }}
              className="text-lg mx-3"
            >
              /
            </span>
            <span className="text-lg">10 AM PST</span>
          </div>
          <div
            style={{
              backgroundColor: secondaryColor,
              color: isSecondaryColorDark ? "white" : "black",
            }}
            className="w-4/5 flex items-center px-12 py-1"
          >
            <div className="bg-white rounded-full border border-gray-400">
              <img
                className="object-contain rounded-full"
                src={favicon}
                alt={companyName}
                width={50}
                height={50}
              />
            </div>
            <div className="pl-3">ADMIT ONE</div>
          </div>
        </div>
        <div
          style={{
            borderLeft: `8px dashed ${primaryColor}`,
          }}
          className="bg-white py-8 px-4 text-black text-center [writing-mode:vertical-rl] [text-orientation:mixed]"
        >
          <div className="text-xs">Ticket No.</div>
          <span
            style={{
              borderColor: secondaryColor,
            }}
            className={`border-l-4 text-2xl`}
          >
            #{ticketNo}
          </span>
        </div>
      </div>
    </div>
  );
};

With TailwindCSS, remember that dynamic class names that aren't composed properly won't be compiled. You can address this in a clean way, but for simplicity, I've chosen to use the style attribute.

We add extra zeros to the start of the ticket number to ensure it's 7 digits long using the padStart method on a string.

This feature is a great addition to event registration processes, adding a personalized touch for attendees. Experiment with the code, adapt it to your needs, and integrate it into your projects to see how dynamic ticket generation can elevate your user experience.

Introducing Visual Copilot: a new AI model to convert Figma designs to high quality code in a click.

No setup needed. 100% free. Supports all popular frameworks.

Try Visual Copilot

Share

Twitter
LinkedIn
Facebook
Hand written text that says "A drag and drop headless CMS?"

Introducing Visual Copilot:

A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot
Newsletter

Like our content?

Join Our Newsletter

Continue Reading
AI5 MIN
Introducing Visual Copilot 2.0: Design to Interactive
October 31, 2024
Design Systems8 MIN
Design Systems Explained
October 18, 2024
Visual Editing7 MIN
Visual editing is bridging the gap between developers and designers
October 11, 2024