DEV Community

Cover image for DRY January: Demotivational Posters with the TanStack or Next.js
Jen Looper for Cloudinary

Posted on

DRY January: Demotivational Posters with the TanStack or Next.js

January can be a time when we make all the resolutions to do and be better in the shiny new year. We resolve to do more... more exercise, a more rigorous diet, even more mindfulness, doggone it. But what if we (and hear me out) instead thought about doing LESS. Less struggling against those 4PM sugar cravings, less fighting to not take an after-lunch nap, less battling frozen driveways to back the car out so you can make it to pilates.

I don't know if this is creepy or inspiring, but let's build it anyway

I don't know if this is creepy or inspiring, but let's build it anyway

What if we embraced our inner zen and simply...did the best we could? After all, isn’t all the productivity that AI is bestowing upon us freeing up our time to do less, rather than more? I’m only half-kidding here.

To encourage us to embrace a reductionist mentality, I did a little experiment to see if I could rethink some popular messaging that I’m sure you encountered if you ever worked in an American corporate office in the early 2000s: motivational posters.

These little gems were plastered on the walls to exhort us to Bring Our Best Selves To Work, Achieve More, and Be Bold. Here’s an example from the “Successories” company:

Leadership Wolfs FTW

As you can see, they have a particular format of a black background, an inspiring photo, an uplifting word with a lot of kerning, and a blurb at the bottom to drive the message home. A Whole Vibe was created as you walked down a poster-adorned corridor in order to toss your bag lunch into the office fridge.

I particularly like this wolf. This is me.

skeptical wolf

Even back in those days, people made fun of these posters, and I remember being employed in one company packed with sarcastic Eastern Europeans that was instead decorated with demotivational posters such as those crafted by the brilliant Despair.inc. Here’s a good one:

lewwwwwsers

The problem with all of these posters is that, well, you have to buy them, and that’s no fun. So let’s see if we can build something like this ourselves, and actually make it a more surprising experience by connecting two APIs to generate shady messaging like this at random.

You Got This fr fr

not sure what 'this' is, but you got it!

Temporarily shelve that resolution against unproductive and negative thinking and let’s see what we can build using:

  • The Affirmations API
  • Unsplash API
  • TanStack for a web frontend
  • Cloudinary image storage to display the image so you can print off a PDF of your poster

You'll need two API keys, one for the ever-useful affirmations.dev API and one for Unsplash images. You'll also need a Cloudinary account, so create one for free here if you aren't already setup. You then need to gather your Cloudinary API key, secret, cloud name and build an upload preset so that you can upload images neatly into Cloudinary. Learn how to get credentials here and how to create an upload preset here.

We’re going to grab one of the pithy affirmations from the API, extract the longest word as the central message to display with plenty of kerning, and then send that word to Unsplash to find a match. Store the image returned in Cloudinary and display the lot a as a printable poster, as a stacked image, word, and affirmation on a black background, like this one:

The miracle of survival

Continuing my tradition of building useless things to try new stacks, let’s give TANStack a whirl. TanStack is a React framework that's pitched as a less 'magical' alternative to Next.js. I also built this same app using Next.js, to compare the two codebases, for learning purposes. Check out the GitHub repo with the two apps here

Disclosure; I used Cursor to help build the two comparable apps in the demo repo. According to Cursor, the difference between the two boils down to your personal dev preference: "Next.js is a full-stack framework with conventions and a large ecosystem. TanStack Router/Start is a type-safe, flexible routing solution that can be extended to full-stack. The choice often comes down to preferring conventions (Next.js) vs. flexibility and type safety (TanStack)."

Some differences between these two frameworks in the context of this app include:

Server-side data fetching architecture

Next.js:

  • Uses Server Actions ('use server') and async Server Components
  • Server actions in actions.ts are callable from both server and client TanStack:
  • Uses route loaders and createServerFn for server functions
  • Data fetching happens in the route's loader function
  • Loaders run on the server before the component renders

Environment variable access

Next.js:

  • Uses process.env.* for environment variables
  • Variables are available on the server by default TanStack:
  • Uses import.meta.env.VITE_* (Vite convention)
  • Only variables prefixed with VITE_ are exposed to the client buildCloudinaryUrl.ts, because this app was built with Vite as its build tool.

Difference in image handling

Next.js:

  • Images are displayed using the built-in optimized component TanStack:
  • TanStack doesn't have such a component built-in, so you need to make sure to optimize your images! This is consistent with its 'magic-free' approach.

Guess what! You can easily optimize your images in Cloudinary. While there are other ways to do this, I decided to just add some URL transformations to the Cloudinary URL returned once the Unsplash image is uploaded:

export function buildCloudinaryImageUrl(publicId: string): string {
  const cloudName = import.meta.env.VITE_CLOUDINARY_CLOUD_NAME

  if (!cloudName) {
    throw new Error('Cloudinary cloud name not configured')
  }

  const plainPublicId = publicId

  const transformations = [
    `c_fill,w_1200,h_800`, // Fill to 1200x800 for poster feel
    `f_auto`, // Auto format
    `q_auto`, // Auto quality
  ].join('/')

  // Return the complete Cloudinary URL with transformations `https://res.cloudinary.com/${cloudName}/image/upload/${transformations}/${plainPublicId}`
}
Enter fullscreen mode Exit fullscreen mode

Now you're able to grab an affirmation from the API and use it to query Unsplash, storing the result into Cloudinary and using it to build your poster. In TANStack it looks like this:

export const fetchNewAffirmationData = createServerFn({
  method: 'GET',
}).handler(async (): Promise<AffirmationData> => {
  // First fetch the affirmation
  const affirmationData = await getAffirmation()

  // Extract the longest word from the affirmation to use as image search query
  const words = affirmationData.affirmation.split(' ').filter(word => word.length > 0)
  const longestWord = words.reduce((longest, current) => 
    current.length > longest.length ? current : longest, ''
  ).toLowerCase()

  // Fetch an image based on the longest word
  const accessKey = import.meta.env.VITE_UNSPLASH_ACCESS_KEY
  if (!accessKey) {
    throw new Error('Unsplash access key not configured')
  }

  const unsplashResponse = await fetch(
    `https://api.unsplash.com/photos/random?client_id=${accessKey}&orientation=landscape&query=${encodeURIComponent(longestWord)}`
  )
  if (!unsplashResponse.ok) {
    throw new Error('Failed to fetch Unsplash photo')
  }
  const unsplashData = await unsplashResponse.json() as { id: string; urls: { full: string } }

  // Upload the Unsplash image to Cloudinary
  const publicId = await uploadToCloudinary(unsplashData.urls.full)

  // Build the Cloudinary URL with transformations
  const cloudinaryUrl = buildCloudinaryImageUrl(publicId)

  return {
    affirmation: affirmationData.affirmation,
    longestWord,
    cloudinaryUrl,
    photoId: unsplashData.id,
    publicId,
  }
})
Enter fullscreen mode Exit fullscreen mode

And once you have all your pieces in place, you'll be able to view that matching affirmation, image, and printable styling all in one place so you can build a PDF poster:

const handlePrint = () => {
    if (posterRef.current) {
      const printWindow = window.open('', '_blank')
      if (printWindow) {
        printWindow.document.write(`
          <!DOCTYPE html>
          <html>
            <head>
              <title>Affirmation Poster</title>
              <style>
                ...some fancy styling
              </style>
            </head>
            <body>
              <div class="poster-container">
                <img src="${imageUrl}" alt="${affirmation}" class="poster-image" />
                <h1>${longestWordWithDots}</h1>
                <div class="poster-line"></div>
                <h2>${affirmation}</h2>
              </div>
            </body>
          </html>
        `)
        printWindow.document.close()

        // Wait for image to load before printing
        setTimeout(() => {
          printWindow.focus()
          printWindow.print()
          printWindow.onafterprint = () => printWindow.close()
        }, 250)
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

So there you have it...you've built a delightfully useless little app that combines a few API calls with optimized image presentation to make you feel a little less seasonally-affected, we hope.

Happy New Year! If you liked this little app, there are plenty more where it comes from from your friends at Cloudinary Developer Relations.

In boca leone

Cloudinary ❤️ developers
At Cloudinary, we work on tools and practices that help developers deliver high-quality visual experiences at any scale. This article was brought to you by the Developer Relations Team at Cloudinary.
👉 Discover more

Top comments (0)