DEV Community

Cover image for Making GraphQL Codegen Work For You: GraphQL Integration with React and TypeScript
Carl-W for novu

Posted on • Originally published at novu.co

Making GraphQL Codegen Work For You: GraphQL Integration with React and TypeScript

TL;DR

In this guide, we’ll be showing you how to use GraphQL alongside React and GraphQL Codegen to create a simple page that can pull data from an API and send emails. We’ll be using Novu as an open source notification system for developers that can send our emails after being passed through a form created within React.

Intro

We’ve all been there: you’ve been coding away in React with Typescript enjoying type-checking until it’s time to time to integrate your application with an API. And then, all of your data types are wrong, ending up with a ton of errors, or just not working at all. Thankfully, integrating APIs doesn’t need to be complicated with GraphQL. It enables you to quickly get an API integrated without the hassle of a standard REST API, thanks to tools like Codegen.

In this article, I will demonstrate the problem that many developers face and how to improve the workflow when integrating your react app with a GraphQL API.

This tutorial assumes that you have been working with React before and want to improve, so you already have your workstation set up.

GraphQL Meme

What We Will Do:

  • Bootstrap a new Next.js TypeScript project
  • Install Apollo GraphQL and fetch data from a public API
  • Showcase why using GraphQL Codegen is a good idea
  • Create a Next.js API endpoint for sending emails with data
  • Use the Novu.co platform for setting up and sending emails to a given email

If you want to skip the writeup, you can go to GitHub directly and look at the code: https://github.com/ugglr/next-typescript-graphql-integration

Creating an Example Project with React and Next.js

Learning is faster when you get your hands dirty, so let’s jump straight in. We’ll bootstrap a new React project using Next.js with Typescript. Next.js is a full-stack framework for React that allows us to quickly start up a web app.

Run the following command in your terminal to scaffold a new Next.js project with Typescript:

yarn create next-app --typescript
Enter fullscreen mode Exit fullscreen mode

It will then prompt you for the name of the project. Let’s name it next-typescript-graphql-integration so we know what we’re working with, and we’ll leave anything else to default values.

While we’re still in the terminal, navigate to our newly created project and run it to make sure that the installation was successful.

cd next-typescript-graphql-integration

yarn dev
Enter fullscreen mode Exit fullscreen mode

The output will be similar to this:

yarn dev
yarn run v1.22.15
$ next dev
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
event - compiled client and server successfully in 1457 ms (165 modules)
Enter fullscreen mode Exit fullscreen mode

From this, we can determine that the page is available at localhost:3000. Opening the browser and checking the page will show the default Next.js screen.

Screenshot of Next.JS Default Page

Install Dependencies & Initialize Apollo Client

We are now ready to install some packages. We start by installing one of the most popular GraphQL clients, Apollo Client. Apollo is a tool that enables devs to utilize GraphQL APIs within almost any tech stack and integrates it within your UI. Then, we’ll start doing some fetching from this public Rick & Morty GraphQL API.

yarn add @apollo/client graphql
Enter fullscreen mode Exit fullscreen mode

When that’s done, let’s open up our code editor and initialize the Apollo Client. When inspecting the folder you can see that it’s a standard bare-bones Next project with the following folder structure:

root
--- .next
--- pages
--- public
--- styles
Enter fullscreen mode Exit fullscreen mode

Inside of _app.tsc we can start initializing our client by following the steps described by the official Apollo Client React docs.

We include the necessary imports and create the client, then include it in our React app via ApolloProvider. In the end, the file should look something like this:

import "@/styles/globals.css";
import type { AppProps } from "next/app";

import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";

const client = new ApolloClient({
  uri: "https://rickandmortyapi.com/graphql",
  cache: new InMemoryCache(),
});

export default function App({ Component, pageProps }: AppProps) {
  return (
    <ApolloProvider {...{ client }}>
      <Component {...pageProps} />
    </ApolloProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is everything we need to initialize our brand new GraphQL client on the client side. 🚀 Let’s continue with fetching some data the un-safe way.

Fetching Data without Type Checking

Now that we have our project up and running, it’s time to add some data from our API. If we want to get a list of characters from the API we need to run a query like so:

query {
  characters {
    results {
      id
      name
            species
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The Apollo documentation will tell us to create the query document and then use it together with the useQuery hook provided by Apollo. I’ve deleted everything in index.tsx and replaced it with the following:

import { gql, useQuery } from "@apollo/client";
import { NextPage } from "next";

const GET_CHARACTERS = gql`
  query {
    characters {
      results {
        id
        name
                species
      }
    }
  }
`;

const Home: NextPage = () => {
  const { loading, data } = useQuery(GET_CHARACTERS);

  return (
    <div>
      <main>
        {loading && <p>Loading...</p>}

        {data?.characters.results.map((character) => (
          <p key={character.id}>{character.name}</p>
        ))}
      </main>
    </div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

This is what it will look like, so let’s check back to our application:

Image description

Everything seems like it’s good, right?

Image description

What Went Wrong with GraphQL?

While our code may work as intended, it’s not exactly reliable. There are two common errors we made here that we need to look at:

No GraphQL Type Validation

We didn’t add any type validation to our GraphQL calls. This means if you request a field that doesn’t exist in the schema, you will get weird and hard-to-understand error messages. Let’s see what can happen if we change our project:

const GET_CHARACTERS = gql`
  query {
    characters {
      results {
        id
        name
        species
                notInSchema // 👈 this will result in error
      }
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

Running this will give us an “Error! Response not successful: Received status code 400”, which doesn’t tell you what went wrong at all, becoming a huge time waster.

Considering that some queries and mutations might have a variety of variables, fields, and subfields this gets complex fast (believe me, I’ve done this to my breaking point before!). So you should be ensuring that your GraphQL queries don’t call for items not in the schema.

No Types In The Response

We also didn’t include any form of autocomplete in our response, so our code doesn’t know what is supposed to be returned from our API. We need to ensure that we validate the data we receive from our API to ensure our data is correct. If we don’t, this can become a nightmare in the future.

Let’s take a look at our code editor. You should see a red line under the character variable with the following Typescript validation error:

Image description

So how do we fix this issue?

If we’re just building something small, we can create our own type like so:

type GetCharactersQueryResponse = {
  characters: {
    results: Array<{
      id: string;
      name: string;
            species: string;
    }>;
  };
};
Enter fullscreen mode Exit fullscreen mode

And then passing it into our query hook:

const { loading, error, data } =
    useQuery<GetCharactersQueryResponse>(GET_CHARACTERS);
Enter fullscreen mode Exit fullscreen mode

This will give us type-checking from Typescript and work just fine.

Image description

But this solution becomes a nightmare when return types become large or if you need to pass variables. All of those are checked in the GraphQL API and will send you confusing errors that are hard to debug.

Fortunately, there are some smart people who realized that all this information is already available inside GraphQL schemas & can be used to generate this information for us.

Enter: GraphQL Codegen 🚀

What Is Graphql Codegen?

Graphql Codegen is a code generation library for GraphQL that enables developers to generate custom code. It provides us developers with the ability to generate type definitions, query builders, documentation, and more by analyzing our GraphQL schemas. This makes it easier and faster to build GraphQL applications and reduces the time spent coding.

Additionally, Graphql Codegen is also a type-safe code generator. This means that the generated code is checked for errors by the Graphql Codegen compiler before it is used in an application. This ensures that any errors in the code are caught before they cause problems in production.

Plus, it also provides developers with an easy way to manage GraphQL schemas. It allows devs to easily add, remove, and update their schemas without any hassle. This makes it much easier to keep your schemas up-to-date.

Setup and Configuring GraphQL Codegen

Before we can start using Graphql Codegen, we’ll need to configure it. This process is relatively simple and can be done in a few steps:

The first step is installing GraphQL Codegen by running this command 👨‍💻

yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-graphql-request @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
Enter fullscreen mode Exit fullscreen mode

This will install the following packages as devDepencencies

  • @graphql-codegen/cli
  • @graphql-codegen/typescript
  • @graphql-codegen/typescript-graphql-request
  • @graphql-codegen/typescript-operations
  • @graphql-codegen/typescript-react-apollo

After installing, we need to configure GraphQL Codegen. I prefer to do this by adding a .yml file to the root of our directory.

$ touch graphql.config.yml
Enter fullscreen mode Exit fullscreen mode

Open up the file and let’s add the following configuration:

schema:
  - "https://rickandmortyapi.com/graphql" 
documents:
  - "./graphql/**/*.graphql"
generates:
  ./generated/graphql.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
Enter fullscreen mode Exit fullscreen mode

The configuration file does the following:

  • schema This points to our GraphQL endpoint for fetching the API schema map.
  • documents This tells GraphQL Codegen where to look for our schema files
  • generates This tells GraphQL Codegen where to create and store generated code.
  • plugins Specifies what GraphQL Codegen plugins to use.

To finalize the setup we also need to add the generation script to our package.json

{
  "name": "next-typescript-graphql-integration",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "generate": "graphql-codegen --config graphql.config.yml" 👈 here
  },
  "dependencies": {
    "@apollo/client": "^3.7.7",
    "@next/font": "13.1.6",
    "@types/node": "18.11.18",
    "@types/react": "18.0.27",
    "@types/react-dom": "18.0.10",
    "eslint": "8.33.0",
    "eslint-config-next": "13.1.6",
    "graphql": "^16.6.0",
    "next": "13.1.6",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "typescript": "4.9.5"
  },
  "devDependencies": {
    "@graphql-codegen/cli": "^3.0.0",
    "@graphql-codegen/typescript": "^3.0.0",
    "@graphql-codegen/typescript-graphql-request": "^4.5.8",
    "@graphql-codegen/typescript-operations": "^3.0.0",
    "@graphql-codegen/typescript-react-apollo": "^3.3.7"
  }
}

Enter fullscreen mode Exit fullscreen mode

If you run yarn generate now you will see an error output like this:

yarn generate
yarn run v1.22.15
$ graphql-codegen --config ./graphql.config.yml
(node:15632) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
✔ Parse Configuration
❯ Generate outputs
✔ Parse Configuration
⚠ Generate outputs
  ❯ Generate to ./generated/graphql.ts
    ✔ Load GraphQL schemas
    ✖
      Unable to find any GraphQL type definitions for the f…
      - ./graphql/**/*.graphql
    ◼ Generate
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Enter fullscreen mode Exit fullscreen mode

This means the setup is complete and we are ready to add some schema files.

Adding Our Schema File

Starting off, create a folder in the root of the directory called graphql, and inside create a file called get-characters.query.graphql. We already told Codegen where to look for our schemas, but the second filename is up to you.

The folder structure now looks like this 👇

root
--- .next
--- pages
--- public
--- styles
--- graphql 👈 here 🤩
Enter fullscreen mode Exit fullscreen mode

Then inside of the file get-characters.query.graphql, add the query that we previously used, but without wrapping it in a gql template string, like this:

query GetCharacters {
  characters {
    results {
      id
      name
      species
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And then re-run the generate script, and you will see the following:

yarn generate
yarn run v1.22.15
$ graphql-codegen --config ./graphql.config.yml
(node:16009) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
✔ Parse Configuration
❯ Generate outputs
✔ Parse Configuration
✔ Generate outputs
✨  Done in 7.43s.
Enter fullscreen mode Exit fullscreen mode

Congratulations! We are almost done. Now, check root and you will notice that the script has created a new folder called generated with a file called graphql.ts. This is as we configured in the graphql.config.file.

root
--- .next
--- pages
--- public
--- styles
--- graphql 
--- generated 👈 here 👀
Enter fullscreen mode Exit fullscreen mode

If you inspect that file you can see that we have generated a bunch of types pulled from the API and generated custom React hooks for the query that we authored in get-characters.query.graphql.

GraphQL Codegen in Action

Now that you have Graphql Codegen set up and configured, you can start using it in your React application. Let’s modify index.tsxto the following:

import {
  GetCharactersDocument,
  GetCharactersQuery,
  GetCharactersQueryVariables,
} from "@/generated/graphql";
import { useQuery } from "@apollo/client";
import { NextPage } from "next";

const Home: NextPage = () => {
  const { loading, data } = useQuery<
    GetCharactersQuery,
    GetCharactersQueryVariables
  >(GetCharactersDocument);

  const characters = data?.characters?.results;

  return (
    <div>
      <main>
        {loading && <p>Loading...</p>}

        {characters &&
          characters.map((character, index) => (
            <p key={character?.id ?? index}>
              {character?.name ?? "No name: something is wrong"}
            </p>
          ))}
      </main>
    </div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

And now, our data is type-safe and validated. If there was something wrong within the query that didn’t match the API, it would have thrown an error during generation. Now, inside our component, we now have fully typed data, which can be demonstrated by hovering over character.

Image description

But where are the custom hooks?

Let’s take it one step further and use the typescript-react-apollo plugin we told GraphQL Codegen to use. This will make our code super clean.

Using Generated End-To-End Type-Safe Apollo Hooks

Let’s clean up our code a little bit to something like this:

import { NextPage } from "next";
import { useGetCharactersQuery } from "@/generated/graphql";

const Home: NextPage = () => {
  // This hook is validated towards the API & will infer 
  // types and throw you type errors if you have not
  // provided the right variables.
  const { loading, data } = useGetCharactersQuery();

  const characters = data?.characters?.results;

  return (
    <div>
      <main>
        {loading && <p>Loading...</p>}

        {characters &&
          characters.map((character, index) => (
            <p key={character?.id ?? index}>
              {character?.name ?? "No name: something is wrong"}
            </p>
          ))}
      </main>
    </div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

Look at how neat and clean that is! 🤩 And it’s still typed, too!

Image description

For this project to continue API integration, all developers need to do is add new files into the graphql folder and run yarn generate before restarting the dev server. Then, everything gets generated automatically.

One more check to see if it works. Don’t forget to start your development server if you stopped it.

Image description

Obviously, this is a very small app so far. Even so, I think I have been able to demonstrate that even at this scale, GraphQL Codegen is a very valuable tool.

Best Practices for GraphQL Codegen

There are some best practices that developers should follow when using GraphQL Codegen:

First, it is important to keep your GraphQL Codegen configuration file up-to-date. This will ensure that the generated code is always accurate. It’s also important to make sure your configuration file is well-structured and easy to understand. This makes it easier for changes in the future.

Second, for production projects, it is important to test your generated code. This ensures the code is working as expected and any errors are caught before they cause problems in production.

Adding In New Functionality With Novu

Showing data is pretty neat, but what if we could aggregate that data and send it to our users?

For this, we can use Novu!

Novu is an open-source notification infrastructure for developers. They make it possible to send notifications though a unified API. With their platform, it’s possible to bundle notification sending into “triggers”.

Managing all notification handlers normally becomes a big chore for us developers as our applications become more complex. Only handling code for email notifications might be doable in a small team, but adding in SMS and push notifications quickly becomes a time drain to get up and running. Which is exactly what Novu intends to help with.

How to Get Started with Novu

The quickest way to get started with Novu is by signing up for their cloud platform. Its totally free, and with a generous free tier of 10k events per month, you’ll have plenty to work with. There is also the option to self-host the platform on your own servers but will require extra work. For this tutorial, we’ll be going with the simpler option through Novu’s cloud platform.

Step 1: Sign Up

After signing up they will ask for an organization name, and then you will be greeted with a welcome screen.

Step 2: Connect the Novu Email Provider

For most providers, Novu sits between them and your application. That means if you are sending an SMS to your user, Novu doesn’t send that, it’s done by your provider.

However, to get started, I recommend using their built-in email provider. It lets you send up to 300 emails, which is plenty for small applications like ours. The emails will come from no-reply@novu.co, and the sending will be the organization name you picked earlier.

Image description

Image description

Step 3: Create a New Notification Trigger

We also need to define a trigger by going to Notifications in the cloud panel.

Image description

Press New and add our trigger.

This is what our settings should look like, but feel free to switch it up if you want:

Image description

Then head into the Workflow Editor and configure what happens when triggered.

Image description

Add an email field and then enter the email editor to edit the template or any other settings you’d like to add.

Email templates often inject variables with {{ VARIABLE }}. This is how we’ll inject the request payload into our email template. Apply variables in the editor, and you’ll see them show up in the variables box. In this case, {{ name }} and {{ species }} will need to be present in the payload we send to the API. This is what our template should look like:

Image description

Press update and see that the payload variables show up in the variables box:

Image description

That’s all we need for now! Let’s continue with writing the components and wire everything together. 🪡

Step 4: Create the React Form Component

Let's jump back into the code and create a form to take our users’ email and store it locally.

We will create a folder in root called components, with a file called EmailForm.tsx.

import { FormEvent, useState } from "react";

const EmailForm = () => {
  const [success, setSuccess] = useState<boolean>(false);
  const [email, setEmail] = useState<string>("");

  const onSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // send the email to user
  };

  return (
    <div>
      <p>Send me random character 🚀</p>
      <form onSubmit={onSubmit}>
        <input
          type="text"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        <button type="submit">Send!</button>
      </form>
    </div>
  );
};

export default EmailForm;
Enter fullscreen mode Exit fullscreen mode

With our form set up and ready to go, we can connect it to the Novu API!

Step 5: Install Novu-client

The easiest way to interact with the Novu API is to install their client package. We can install the @novu/node package from npm by running yarn add @novu/node.

After this, create a folder in root called lib which will hold the logic for interacting with the Novu API. We’ll call it novu.ts, and it’s going to export an async function which we can then re-use.

Then, we’ll head into the Novu Dashboard and grab this Node.js code snippet:

import { Novu } from '@novu/node'; 

const novu = new Novu('<API_KEY>');

novu.trigger('randm-random-email', {
    to: {
      subscriberId: '<REPLACE_WITH_DATA>',
      email: '<REPLACE_WITH_DATA>'
    },
    payload: {
      name: '<REPLACE_WITH_DATA>',
      species: '<REPLACE_WITH_DATA>'
    }
  });
Enter fullscreen mode Exit fullscreen mode

Pretty neat! They’ve done most of the work for us ☕️. Novu adds the payload variables and everything, saving us time from going to the docs.

Now back to the editor:

// lib/novu.ts
import { Novu } from "@novu/node";

const novu = new Novu("<API_KEY>");

type Payload = {
  name: string;
  species: string;
};
export const sendEmail = async (email: string, payload: Payload) => {
  if (!email) throw new Error("No email");

  await novu.trigger("<REPLACE_WITH_TRIGGER_ID>", {
    to: {
            email,
      subscriberId: email,
    },
    payload,
  });
};
Enter fullscreen mode Exit fullscreen mode

This is how the payload should look in the new Payload type, which will be sent to Novu and injected into our email template. Replace API_KEY and TRIGGER_ID with our variables, and we should be good to go!

Note: Before pushing this to git, please make sure you’re using environmental variables, not what’s shown in this tutorial.

Step 6: Create a Next.js API Endpoint

Next.js is a full-stack framework for React, and they make it easy for us to create full-stack apps. One of the most notable features is that we can create API endpoints on our server. These are then hidden from the client browser for extra security.

The way these works is by adding a new file to our pages folder and using the folder structure to define our endpoints. The below structure will create an endpoint at /send-email :

root
--- .next
--- pages
        --- api
                --- hello.ts
                --- send-email.ts
--- public
--- styles
--- graphql 
--- generated

Enter fullscreen mode Exit fullscreen mode

And here’s the code for the handler, where we get our parameters from the request:

import { sendEmail } from "@/lib/novu";
import { NextApiRequest, NextApiResponse } from "next";

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  // we are only handling post requests.
  if (req.method === "POST") {
    try {
      const { email, name, species } = JSON.parse(req.body);

      if (!email || !name || !species) {
        // return bad request status.
        res.status(400).end();
        return;
      }

      await sendEmail(email, { name, species });

      res.status(200).end();
    } catch (error) {
      // Just response internal server error;
      res.status(500).end();
    }
  }
};

export default handler;
Enter fullscreen mode Exit fullscreen mode

This handler will:

  1. Look at any incoming POST requests to the endpoint
  2. Check if the body of the request has the necessary params
  3. Send the email if everything is correct, or return an error if something is wrong

And that’s all we need for our backend! So let’s add in the client-side.

Step 7: Add Our Email Functionality

Now that we’re back at the client-side, let’s add in the rest of the functionality. We’ll start at the top of the tree in index.tsx and import our EmailForm.tsx component like so:

import { NextPage } from "next";
import { useGetCharactersQuery } from "@/generated/graphql";
import EmailForm from "@/components/EmailForm";

const Home: NextPage = () => {
  const { loading, data } = useGetCharactersQuery();

  const characters = data?.characters?.results;

  return (
    <div>
      <main>
        {loading && <p>Loading...</p>}

        {characters && (
          <div>
            <EmailForm {...{ characters }} />

            {characters.map((character, index) => (
              <p key={character?.id ?? index}>
                {character?.name ?? "No name: something is wrong"}
              </p>
            ))}
          </div>
        )}
      </main>
    </div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

That was simple! Let’s finish the implementation in EmailForm.tsx :

import { Character } from "@/generated/graphql";
import { FormEvent, useState } from "react";

type Props = {
  characters: (Character | null)[] | null | undefined;
};

const EmailForm: React.FC<Props> = ({ characters }) => {
  const [email, setEmail] = useState<string>("");

  const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (characters && characters?.length > 0) {
      // Get random character from the array.
      const random = Math.floor(Math.random() * (characters.length - 1));
      const randomCharacter = characters[random];
      // Make send email request to /api/send-email
      fetch("http://localhost:3000/api/send-email", {
        method: "POST",
        body: JSON.stringify({
          email,
          name: randomCharacter?.name,
          species: randomCharacter?.species,
        }),
      });
    }
  };

  return (
    <div>
      <p>Send me random character 🚀</p>
      <form onSubmit={onSubmit}>
        <input
          type="text"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        <button type="submit">Send!</button>
      </form>
    </div>
  );
};

export default EmailForm;
Enter fullscreen mode Exit fullscreen mode

This form component will:

  1. Render a text input for email, and store the data in local storage
  2. Submit the data when the button has been pressed
  3. Makes a request to our API which sends a request to Novu.co, which in turn will send en email to the given email address

And that’s it! Now we need to test it out.

Step 8: Send Emails!

If we take a look in the browser at this point, it should look like this:

Image description

It’s not pretty, but it’s functional!

Enter your email and see if it shows up in your inbox!

Image description

Wrapping It Up

Integrating APIs into a project can be difficult, but it doesn’t have to be. And with GraphQL, it’s easier than ever before! It allows you to set up API integrations with ease and can pair with tools like GraphQL Codegen to automate some of the hassles.

Paired with a powerful notification platform like Novu, you can easily get a project up and running in no time, with notifications and communication options at your disposal.

And the best part? You can do it all for free! No buy-in required. I know I’ll be using Novu in my future, what about you?

Full source code can be found here https://github.com/ugglr/next-typescript-graphql-integration

Top comments (2)

Collapse
 
nevodavid profile image
Nevo David

Great article 🚀

Collapse
 
ugglr profile image
Carl-W

Thank you 🤩