Shopify Remix Discount App Tutorial (Part 3/3)

Shopify Remix Discount App Tutorial (Part 3/3)

Learn how to create a discount App using the Shopify Remix App Framework

ยท

14 min read

Intro

Welcome back! Today we will build a user Interface for the Shopify discount function created in part 1 and part 2 of this series. There is a lot to cover so let's jump straight into it!

Development

Cleaning up Home page (optional)

Start the development server and run npm run dev at the root of your project. Currently, our home page looks like this:

Open app/routes/app._index.jsx and replace its content with:

import {
  Page,
  Text,
  Card,
  BlockStack,
  InlineStack,
  Link,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";

export const loader = async ({ request }) => {
  await authenticate.admin(request);

  return null;
};

export default function Index() {
  return (
    <Page>
      <Card>
        <BlockStack gap="500">
          <BlockStack gap="100">
            <InlineStack>
              <Text as={"h2"} variant="headingLg">
                Customer Tag Discount App
              </Text>
            </InlineStack>
            <InlineStack>
              <Text as={"p"}>
                Navigate to Store Discount page and create a new discount
              </Text>
            </InlineStack>
          </BlockStack>
          <Link target="_self" url={`shopify:admin/discounts`}>
            Go to Discounts
          </Link>
        </BlockStack>
      </Card>
    </Page>
  );
}

Now, that looks much cleaner, and at least gives the merchant that opens it some context of what it does.

๐Ÿ’ก
You can read the documentation here if you are unfamiliar with how Remix routing works.

Create Discount Page

Navigate to your store Discount page and attempt to create a new one.

You will notice that your new Discount is there! Let's Click on it and see what happens... Did you get redirected to your app homepage? Yes? Good! That's expected behavior (for now at least).

Configure the create/details UI path for your function

Shopify allows us to define a route to which our Discount should redirect merchants when they create a new one or try to view details of already existing ones.

Open your extension shopify.extension.toml file again, you should find there [extensions.ui.paths] settings, update it to look like this:

  [extensions.ui.paths]
  create = "/app/customer-tag-discount/:functionId/new"
  details = "/app/customer-tag-discount/:functionId/:id"

Now when a user tries to create a new or view details of an existing discount they will be redirected to those paths in your app.

๐Ÿ’ก
:funcitonId and :id in your extension routes are dynamic tokens. Shopify automatically fills those in with values so we can use them in our app.

Create discount page

Create a new file in your app routes folder: app.customer-tag-discount.$functionId.new.jsx and paste in the below code, as in previous posts I will explain the logic through comments, make sure to read all of them!

import {
  Card,
  Layout,
  List,
  Page,
  Text,
  BlockStack,
  ButtonGroup,
  Button,
  InlineStack,
  FormLayout,
  TextField,
  Tag,
  InlineGrid,
  Banner,
  Thumbnail,
  RadioButton,
  InlineError,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
import { useCallback, useEffect, useState } from "react";
import { ImageIcon, PlusIcon } from "@shopify/polaris-icons";
import { useSubmit, useActionData, useNavigation } from "@remix-run/react";
import { json } from "@remix-run/node";

export const loader = async ({ request }) => {
  await authenticate.admin(request);

  return null;
};

export const action = async ({ request, params }) => {
  // -------
  // 1. Authenticate the request, and get the admin client
  // -------

  const { admin } = await authenticate.admin(request);

  // -------
  // 2. Get the functionId from the params
  // -------

  const { functionId } = params;

  // -------
  // 3. Parse the form data
  // -------

  const formData = await request.formData();

  // -------
  // 4. Get the discount data from the form data
  // -------

  const {
    title,
    tags,
    discountValue,
    products,
    productsDetails,
    discountType,
  } = JSON.parse(formData.get("discount"));

  // -------
  // 5. Create the base discount configuration object
  // -------

  const baseDiscount = {
    functionId,
    title,
    startsAt: new Date(),
  };

  // -------
  // 6. Create the discount in the Shopify admin and get the response
  // -------

  const response = await admin.graphql(
    `mutation discountAutomaticAppCreate($automaticAppDiscount: DiscountAutomaticAppInput!) {
      discountAutomaticAppCreate(automaticAppDiscount: $automaticAppDiscount) {
        userErrors {
          code
          message
          field
        }
      }
    }`,
    {
      variables: {
        automaticAppDiscount: {
          ...baseDiscount,
          metafields: [
            {
              namespace: "$app:customer-tag-discount",
              key: "function-configuration",
              type: "json",
              value: JSON.stringify({
                discountMessage: title,
                discountTags: tags,
                discountValue: discountValue,
                discountType: discountType,
                discountProducts: products,
                productsDetails: productsDetails,
              }),
            },
          ],
        },
      },
    },
  );

  const responseJson = await response?.json();

  // -------
  //  7. Get the errors and success from the response
  // -------

  const errors = responseJson?.data?.discountAutomaticAppCreate?.userErrors;
  const success = errors?.length === 0 ? true : false;

  // -------
  //  8. Return errors and success as JSON to the client
  // -------

  return json({ errors, success });
};

export default function CreateDiscount() {
  // -------
  // 1. Get the navigation object from the useNavigation hook, This will be used to check the form method and state in order to show the loading spinner when the form is submitting
  // -------

  const nav = useNavigation();
  const isLoading =
    ["loading", "submitting"].includes(nav.state) && nav.formMethod === "POST";

  // -------
  // 2. Get the submit function from the useSubmit hook, this will be used to submit the form
  // -------

  const submit = useSubmit();

  // -------
  // 3. Get the action data from the useActionData hook, this will be used to display the errors if there are any, or redirect the user to the discounts page if the discount was created successfully
  // -------

  const actionData = useActionData();

  useEffect(() => {
    if (actionData?.success) {
      // https://shopify.dev/docs/api/app-bridge-library/apis/navigation - Learn more about App Bridge navigation
      open("shopify:admin/discounts", "_self");
    }
  }, [actionData, nav]);

  // -------
  // 4. Set the initial state of the form
  // -------

  const [tagInputValue, setTagInputValue] = useState("");
  const [formState, setFormState] = useState({
    title: "Enter Discount Title",
    tags: [],
    discountType: "orderDiscount",
    products: null,
    productsDetails: [],
    discountValue: "",
  });

  // -------
  // 5. Create handle funtions to update the form state when the user interacts with the form
  // -------

  // - handleProductsDiscountChange: updates the discount value
  const handleProductsDiscountChange = useCallback(
    (newValue) => {
      setFormState({ ...formState, discountValue: newValue });
    },
    [formState],
  );

  // - handleChangeMessage: updates the title
  const handleChangeMessage = useCallback(
    (newValue) => setFormState({ ...formState, title: newValue }),
    [formState],
  );

  // - handleRadioButtonsChange: updates the discount type
  const handleRadioButtonsChange = useCallback(
    (_, newValue) => {
      if (newValue === "orderDiscount") {
        setFormState({
          ...formState,
          products: null,
          productsDetails: [],
          discountType: newValue,
        });
      } else {
        setFormState({ ...formState, discountType: newValue });
      }
    },
    [formState],
  );

  // - handleRemoveTag: removes a tag from the tags array
  const handleRemoveTag = useCallback(
    (tag) => () => {
      setFormState({
        ...formState,
        tags: formState.tags.filter((item) => item !== tag),
      });
    },
    [formState],
  );

  // - handleAddTag: adds a tag to the tags array
  const handleAddTag = useCallback(() => {
    if (!tagInputValue || formState.tags.includes(tagInputValue)) {
      return;
    }
    setFormState({
      ...formState,
      tags: [...formState.tags, tagInputValue],
    });

    setTagInputValue("");
  }, [tagInputValue, formState]);

  // - handleSelectProduct: opens the Shopify resource picker to select products
  async function handleSelectProduct() {
    // a. Get the selected products ids
    const selectedIds = formState.productsDetails.map((product) => {
      return {
        id: product.id,
        variants: product.variants.map((variant) => {
          return {
            id: variant.id,
          };
        }),
      };
    });

    // b. Open the Shopify resource picker
    const products = await shopify.resourcePicker({
      multiple: true, // whether to allow multiple selection or not
      type: "product", // resource type, either 'product' or 'collection'
      action: "select", // customized action verb, either 'select' or 'add',
      selectionIds: selectedIds, // currentlySelected resources
    });

    // c. If the user selected products, update the form state with selected variant ids
    if (products) {
      const allVariantsIds = [];
      products.forEach((product) => {
        product.variants.forEach((variant) => {
          allVariantsIds.push(variant.id);
        });
      });

      if (allVariantsIds.length > 0) {
        setFormState({
          ...formState,
          products: allVariantsIds,
          productsDetails: products,
        });
      }
    }
  }

  // -------
  // 6. Handle the form submission
  // -------

  const handleFormSubmit = () => {
    const formData = new FormData();
    formData.append("discount", JSON.stringify(formState));
    submit(formData, { method: "post" });
  };

  // -------
  // 7. Return the UI
  // -------

  return (
    <Page
      title="Customer Tag Discount"
      backAction={{
        onAction: () => {
          open("shopify:admin/discounts", "_self");
        },
      }}
    >
      <Layout>
        {actionData?.errors?.length > 0 ? (
          <Layout.Section>
            <Banner title="Error" tone="warning">
              <p>There were some when creating your Discount:</p>
              <ul>
                {actionData?.errors?.map(({ message, field }, index) => {
                  return (
                    <li key={`${message}${index}`}>
                      {field.join(".")}: {message}
                    </li>
                  );
                })}
              </ul>
            </Banner>
          </Layout.Section>
        ) : null}
        <Layout.Section>
          <FormLayout>
            <Card>
              <BlockStack gap="500">
                <InlineGrid columns="1fr auto">
                  <Text as={"h2"} variant="headingMd">
                    Customer Tag Discount
                  </Text>
                  <Text as={"h2"} variant="regular">
                    Customer Discount
                  </Text>
                </InlineGrid>
                <BlockStack gap="100">
                  <Text as={"p"} variant="regular">
                    Title
                  </Text>

                  <TextField
                    label=""
                    value={formState.title}
                    onChange={handleChangeMessage}
                    autoComplete="off"
                  />
                  <Text as="p" fontWeight="regular">
                    Customers will see this in their cart and at checkout.
                  </Text>
                </BlockStack>
              </BlockStack>
            </Card>
            <Card>
              <BlockStack gap="500">
                <Text as={"h2"} variant="headingMd">
                  Tags
                </Text>
                <BlockStack gap="100">
                  <Text as={"p"} variant="regular">
                    Enter Customer tag
                  </Text>
                  <InlineStack gap={200}>
                    <TextField
                      value={tagInputValue}
                      onChange={setTagInputValue}
                      autoComplete="off"
                      id="tagInput"
                    />
                    <Button icon={PlusIcon} onClick={handleAddTag}>
                      Add
                    </Button>
                  </InlineStack>
                  <InlineError
                    message={
                      formState.tags.includes(tagInputValue)
                        ? "Tag already added"
                        : null
                    }
                    fieldID="tagInput"
                  />
                </BlockStack>
                {formState.tags.length > 0 ? (
                  <InlineStack gap="200">
                    {formState.tags.map((option) => (
                      <Tag key={option} onRemove={handleRemoveTag(option)}>
                        {option}
                      </Tag>
                    ))}
                  </InlineStack>
                ) : null}
                {formState.tags.length != 0 && (
                  <Text as="p" fontWeight="regular">
                    Only Customer with at least one of these tags will get the
                    discount. discount.
                  </Text>
                )}
              </BlockStack>
            </Card>
            <Card>
              <BlockStack gap="500">
                <InlineStack align="space-between">
                  <Text as={"h2"} variant="headingMd">
                    Discount Type
                  </Text>
                  {formState.productsDetails.length > 0 ? (
                    <Button variant="plain" onClick={handleSelectProduct}>
                      Change products
                    </Button>
                  ) : null}
                </InlineStack>
                <BlockStack gap="50">
                  <RadioButton
                    label="Order Discount"
                    checked={formState.discountType === "orderDiscount"}
                    id="orderDiscount"
                    name="discountType"
                    onChange={handleRadioButtonsChange}
                  />
                  <RadioButton
                    label="Products Discount"
                    id="productsDiscount"
                    name="discountType"
                    checked={formState.discountType === "productsDiscount"}
                    onChange={handleRadioButtonsChange}
                  />
                </BlockStack>
                <BlockStack gap="500">
                  {formState.productsDetails.length > 0 &&
                    formState.productsDetails.map((product) => {
                      return (
                        <InlineStack
                          key={product.id}
                          blockAlign="start"
                          gap="500"
                        >
                          <Thumbnail
                            source={product.images[0].originalSrc || ImageIcon}
                            alt={product.images[0].altText}
                          />
                          <BlockStack gap="100">
                            <Text
                              as="span"
                              variant="headingMd"
                              fontWeight="semibold"
                            >
                              {product.title}
                            </Text>
                            <BlockStack gap="100">
                              {product.variants.map((variant, index) => {
                                return (
                                  <Text key={variant.id} as="span" variant="p">
                                    {variant.displayName}
                                    {index !== product.variants.length - 1
                                      ? ", "
                                      : ""}
                                  </Text>
                                );
                              })}
                            </BlockStack>
                          </BlockStack>
                        </InlineStack>
                      );
                    })}
                  {formState.discountType === "productsDiscount" &&
                    formState.productsDetails.length === 0 && (
                      <BlockStack gap="200">
                        <Button
                          onClick={handleSelectProduct}
                          id="select-product"
                        >
                          Select Products
                        </Button>
                      </BlockStack>
                    )}
                </BlockStack>
              </BlockStack>
            </Card>
            <Card>
              <BlockStack gap="500">
                <Text as={"h2"} variant="headingMd">
                  Discount Value
                </Text>

                <BlockStack gap="100">
                  <Text as={"p"} variant="regular">
                    Enter Discount Value 1-100
                  </Text>
                  <TextField
                    label=""
                    type="number"
                    value={formState.discountValue}
                    error={
                      (formState.discountValue <= 0 ||
                        formState.discountValue > 100) &&
                      formState.discountValue != ""
                        ? "Discount value must be between 1 and 100"
                        : null
                    }
                    prefix="%"
                    onChange={handleProductsDiscountChange}
                    autoComplete="off"
                  />
                </BlockStack>
              </BlockStack>
            </Card>
          </FormLayout>
        </Layout.Section>
        <Layout.Section variant="oneThird">
          <BlockStack gap="500">
            <Card>
              <BlockStack gap="500">
                <BlockStack gap="100">
                  <Text as={"h2"} variant="headingLg">
                    Summary
                  </Text>
                </BlockStack>
                <BlockStack gap="100">
                  <Text as={"p"} variant="bold">
                    Type and Method
                  </Text>
                  <List type="bullet">
                    <List.Item>Automatic Discount</List.Item>
                    {formState.discountType === "productsDiscount" && (
                      <List.Item>Discount on Selected Products</List.Item>
                    )}
                    {formState.discountType === "orderDiscount" && (
                      <List.Item>Discount on Entire Order</List.Item>
                    )}
                  </List>
                </BlockStack>
                {formState.discountValue != 0 && (
                  <BlockStack gap="100">
                    <Text as={"p"} variant="bold">
                      Discount Value
                    </Text>
                    <Text as={"p"} variant="regular">
                      {formState.discountValue} %
                    </Text>
                  </BlockStack>
                )}
                {formState?.tags.length > 0 && (
                  <BlockStack gap="200">
                    <Text as={"p"} variant="bold">
                      Applies to customers with tags:
                    </Text>
                    <InlineStack gap="200">
                      {formState.tags.map((option) => (
                        <Tag key={option}>{option}</Tag>
                      ))}
                    </InlineStack>
                  </BlockStack>
                )}
              </BlockStack>
            </Card>

            <InlineStack gap="200" align="end">
              <ButtonGroup>
                <Button
                  onClick={() => {
                    open("shopify:admin/discounts", "_self");
                  }}
                >
                  Discard
                </Button>
                <Button
                  loading={isLoading}
                  variant="primary"
                  onClick={() => handleFormSubmit()}
                >
                  Save discount
                </Button>
              </ButtonGroup>
            </InlineStack>
          </BlockStack>
        </Layout.Section>
      </Layout>
    </Page>
  );
}
๐Ÿ’ก
Learn more about Shopify Polaris and its components.
๐Ÿ’ก
Notice how in the remix action function we perform the discountAutomaticAppCreate GraphQL mutation that we used in part 1 of this series, this time using values from the user form fields, this mutation also allows you to set a metafield at the same time.

Now when you navigate to a discount page, create a new one, and select Customer Tag Discount you should be presented with this page:

Play around with the fields and create a discount, it should work as expected when you visit your store!

๐Ÿ’ก
Notice in the URL that the dynamic token was replaced with function ID
๐Ÿ’ก
AND REMEMBER your function works on the condition that the Customer (you in this scenario) is logged in and has at least one of the tags you add when creating a new discount. Make sure to create an account, add one of those tags to your profile in admin, and log in.

Creating a View Discount Page

Now that we can create a discount it would be nice to also allow merchants to view its details.

๐Ÿ’ก
You could also make it work like with other native Shopify discounts, where merchants can update the discounts. In that case, you could make this page work similar to the create discount one using the discountAutomaticAppUpdate mutation.

In your apps routes folder create a new file: app.customer-tag-products-discount.$functionId.$id.jsx and paste in the below code, as previously you will find comments explaining its logic.

import {
  Card,
  Layout,
  List,
  Page,
  Text,
  BlockStack,
  Button,
  InlineStack,
  Thumbnail,
  FormLayout,
  TextField,
  Tag,
  InlineGrid,
  RadioButton,
  ButtonGroup,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
import { ImageIcon } from "@shopify/polaris-icons";

import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

export const loader = async ({ request, params }) => {
  //---
  // 1. Authenticate the request
  //---

  const { admin } = await authenticate.admin(request);

  //---
  // 2. get discount id from params and format it
  //---
  const { id } = params;
  const ID = "gid://shopify/DiscountNode/" + id;

  //---
  // 3. Fetch the discount metafield passing in the discount id
  //---
  const discount = await admin.graphql(
    `query discountNodes($id: ID!) {
      discountNode(id: $id) {
        metafield(namespace:"$app:customer-tag-discount", key:"function-configuration"){
          value
        }
      }
    }`,
    {
      variables: {
        id: ID,
      },
    },
  );

  const parsedDiscount = await discount.json();

  //---
  // 4. Return the discount details
  //---
  return json({
    discount: parsedDiscount.data,
  });
};

export default function ViewDiscountDetails() {
  //---
  // 1. Use the discount details
  //---

  const data = useLoaderData();

  //---
  // 2. destructure the discount details from the data
  //

  const {
    discountMessage,
    discountTags,
    discountType,
    discountValue,
    productsDetails,
  } = JSON.parse(data?.discount?.discountNode?.metafield?.value);

  //---
  // 3. Return the page UI with the discount details
  //---
  return (
    <Page
      title="Customer Tag Discount"
      backAction={{
        onAction: () => {
          open("shopify:admin/discounts", "_self");
        },
      }}
    >
      <Layout>
        <Layout.Section>
          <FormLayout>
            <Card>
              <BlockStack gap="500">
                <InlineGrid columns="1fr auto">
                  <Text as={"h2"} variant="headingMd">
                    Customer Tag Discount
                  </Text>
                  <Text as={"h2"} variant="regular">
                    Customer Discount
                  </Text>
                </InlineGrid>
                <BlockStack gap="100">
                  <Text as={"p"} variant="regular">
                    Title
                  </Text>

                  <TextField
                    label=""
                    value={discountMessage}
                    autoComplete="off"
                  />
                  <Text as="p" fontWeight="regular">
                    Customers will see this in their cart and at checkout.
                  </Text>
                </BlockStack>
              </BlockStack>
            </Card>
            <Card>
              <BlockStack gap="500">
                <Text as={"h2"} variant="headingMd">
                  discountTags
                </Text>
                {discountTags?.length > 0 ? (
                  <InlineStack gap="200">
                    {discountTags?.map((option) => (
                      <Tag key={option}>{option}</Tag>
                    ))}
                  </InlineStack>
                ) : null}
                {discountTags?.length != 0 && (
                  <Text as="p" fontWeight="regular">
                    Only Customer with at least one of these discountTags will
                    get the discount. discount.
                  </Text>
                )}
              </BlockStack>
            </Card>
            <Card>
              <BlockStack gap="500">
                <InlineStack align="space-between">
                  <Text as={"h2"} variant="headingMd">
                    Discount Type
                  </Text>
                </InlineStack>
                <BlockStack gap="50">
                  <RadioButton
                    label="Order Discount"
                    checked={discountType === "orderDiscount"}
                    id="orderDiscount"
                    name="discountType"
                  />
                  <RadioButton
                    label="Products Discount"
                    id="productsDiscount"
                    name="discountType"
                    checked={discountType === "productsDiscount"}
                  />
                </BlockStack>
                <BlockStack gap="500">
                  {productsDetails.length > 0 &&
                    productsDetails.map((product) => {
                      return (
                        <InlineStack
                          key={product.id}
                          blockAlign="start"
                          gap="500"
                        >
                          <Thumbnail
                            source={product.images[0].originalSrc || ImageIcon}
                            alt={product.images[0].altText}
                          />
                          <BlockStack gap="100">
                            <Text
                              as="span"
                              variant="headingMd"
                              fontWeight="semibold"
                            >
                              {product.title}
                            </Text>
                            <BlockStack gap="100">
                              {product.variants.map((variant, index) => {
                                return (
                                  <Text key={variant.id} as="span" variant="p">
                                    {variant.displayName}
                                    {index !== product.variants.length - 1
                                      ? ", "
                                      : ""}
                                  </Text>
                                );
                              })}
                            </BlockStack>
                          </BlockStack>
                        </InlineStack>
                      );
                    })}
                </BlockStack>
              </BlockStack>
            </Card>
            <Card>
              <BlockStack gap="500">
                <Text as={"h2"} variant="headingMd">
                  Discount Value
                </Text>
                <BlockStack gap="100">
                  <TextField
                    label=""
                    type="number"
                    value={discountValue}
                    prefix="%"
                    autoComplete="off"
                  />
                </BlockStack>
              </BlockStack>
            </Card>
          </FormLayout>
        </Layout.Section>
        <Layout.Section variant="oneThird">
          <BlockStack gap="500">
            <Card>
              <BlockStack gap="500">
                <BlockStack gap="100">
                  <Text as={"h2"} variant="headingLg">
                    Summary
                  </Text>
                </BlockStack>
                <BlockStack gap="100">
                  <Text as={"p"} variant="bold">
                    Type and Method
                  </Text>
                  <List type="bullet">
                    <List.Item>Automatic Discount</List.Item>
                    {discountType === "productsDiscount" && (
                      <List.Item>Discount on Selected Products</List.Item>
                    )}
                    {discountType === "orderDiscount" && (
                      <List.Item>Discount on Entire Order</List.Item>
                    )}
                  </List>
                </BlockStack>
                {discountValue != 0 && (
                  <BlockStack gap="100">
                    <Text as={"p"} variant="bold">
                      Discount Value
                    </Text>
                    <Text as={"p"} variant="regular">
                      {discountValue} %
                    </Text>
                  </BlockStack>
                )}
                {discountTags?.length > 0 && (
                  <BlockStack gap="200">
                    <Text as={"p"} variant="bold">
                      Applies to customers with discountTags:
                    </Text>
                    <InlineStack gap="200">
                      {discountTags?.map((option) => (
                        <Tag key={option}>{option}</Tag>
                      ))}
                    </InlineStack>
                  </BlockStack>
                )}
              </BlockStack>
            </Card>
            <InlineStack gap="200" align="end">
              <ButtonGroup>
                <Button
                  onClick={() => {
                    open("shopify:admin/discounts", "_self");
                  }}
                >
                  Go Back
                </Button>
              </ButtonGroup>
            </InlineStack>
          </BlockStack>
        </Layout.Section>
      </Layout>
    </Page>
  );
}
๐Ÿ’ก
This time we are using the Remix loader function instead of the action as we need discount data available to us when a user loads a page.

Now go to the discounts page and click on the previously created discount, you

should see all its details:

๐Ÿ’ก
Notice that the dynamic tokens were replaced with correct values again. We are only making use of the :id one, if you were to implement the update discount functionality that's where the second param would come into play!

Limitations of automatic discount functions

I think it's important to mention that at the time of writing this blog post, Shopify has a limit on the number of automatic discount functions that you can have live in your store at the same time. The limit is 5, so if you encounter an error trying to create one, double check you have less than that!

Outro

Thanks for sticking around! I hope you will find this tutorial useful and I encourage you not to stop here, but to build on top of what you have already learned.

A couple of improvements that I can think of:

  • Build better error handling so the user can't submit when some fields are empty or incorrect

  • Update View discount details page so it can also update Discounts

  • Increase the number of available fields and customizations like adding the ability to set an end date for the discount or set the Maximum discount uses.

Bye!

ย