You can find a summary of the code updates in this pull request . Read on for explanations.
1. Update route handler
Let’s start by updating the route handler in src/app/api/get-quote-styles/route.ts to (optionally) receive a quote. We’ll plan to use a query string to pass the quote from the client to the route handler – that is, we’ll add this to the end of the URL:
where quoteText
is the text of the quote that the user has entered.
Process query params
Next.js has some docs on picking up query params on the server side. We need to access the request
parameter to the route handler GET
function, and get the searchParams
off of that object.
Here’s what that looks like in src/app/api/get-quote-styles/route.ts:
route.ts
Get random quote, maybe
According to the spec, the button reads “use random quote” when the text input is empty, and “generate styles” when the textarea is populated.
Let’s plan to send the value of the text input as the quote query param if the value is a non-empty string; otherwise we won’t send a query string at all. That way, if the route handler receives the quote query param, we’ll know that’s the quote to use for the OpenAPI message. Otherwise the textarea was empty and we’ll use a random quote.
Let’s remove the incomingQuote
definition and instead use a nullish coalescing operator (??
) to define the quote. Basically, the ??
means:
- if the item on the left side of the operator is not “nullish” (
null
orundefined
), then the expression evaluates to the left item. Note the.get()
function will returnnull
if the key is not present in thesearchParams
, so that’s what we’ll expect if there was no text entered in the UI. - if the item on the left side of the operator is nullish, then the expression evaluates to the right item.
route.ts
The result here: if request.nextUrl.searchParams.get("quote")
is not nullish, then use that value as the quote. Otherwise, the ??
operator returns the expression on the right, which will be a random quote.
Guard against prompt injection
This wasn’t in the spec, but we want to guard against any extra instructions to the AI that someone might try to sneak into their quote (this is called prompt injection ). There’s no foolproof way to prevent prompt injection entirely, but I added this to the end of my system prompt for a bit of safety:
2. Create form component
Just like we started making QuoteContent
(later renamed QuoteDisplay
) when the quote display became complicated, we’re going to make a QuoteForm
component to replace the “use random quote” button and keep src/app/page.tsx’s Home
component high-level.
First, let’s create the new component. Lucky Linux and MacOS users can run this command:
Windows users can use the new-component README as a guide to create the files manually.
This component is going to need the fetchQuoteStyles
return value from the custom useQuoteStyles
hook, in order to get the styles from the route handler. Because of the “start over” button, the component is also going to need another function, something that resets the state of the custom hook.
3. Update custom hook
Add new function
We’ll call this new function startOver
. It looks like this, in src/hooks/use-quote-styles.ts:
use-quote-styles.ts
Update fetchQuoteStyles
We also need to add a parameter to fetchQuoteStyles
, so that the QuoteForm
component can pass along the quote value – and fetchQuoteStyles
can create a query string to pass the quote value along to the route handler.
I used a ternary to specify the baseUrl
with the “quote” query param if incomingQuote
is truthy; otherwise use baseUrl
without a query string (to signal to the route handler that we need a random quote).
I added encodeURI just in case the user puts some non-URL-safe characters in their entered quote.
use-quote-styes.ts
4. Update components
We need to update the Home
and QuoteForm
components for these new hook return values.
Update Home
Let’s update the Home
component in src/app/page.tsx to destructure the new startOver
return value.
We’ll also replace the “use random quote” button with the new QuoteForm
component, and pass both startOver
and fetchQuoteStyles
as props. We can also remove the Button
import, since it’s no longer needed.
page.tsx
This file is nicely high-level — the above code block is the entire file! It’s easy to take a look at this top level page and see the big picture. If we want to drill down, we know where to go for more details.
Update QuoteForm
Time to specify the props for QuoteForm
. The types for the props are a bit mysterious here. In cases like this, I rely on VSCode to tell me the types, by hovering over the item where it’s defined (in this case, in the return value of the hook) and looking at the pop-up for the type:
With help from VSCode, here are the types for the QuoteForm
props:
QuoteForm.tsx
5. Add UI to QuoteForm
All right, it’s time to bring everything together by writing the body of the QuoteForm
component.
Elements
Let’s create a top-level form for the component. We’ll add the general elements: a textarea for the quote input, and two Button
components. For now, we’ll have the second button read “use random quote”; we’ll add code later so the button text changes once a quote is entered.
QuoteForm.tsx
Ugly, but it’s a start:
Layout styling
We’ll use two flex containers here:
- the layout as a whole. This will use the
.wrapper
class, and will be acolumn
flex layout. If we setalign-items: stretch
then all of the items (textarea and buttons) will take up their entire row. - the buttons row. We’ll create a
div
around the buttons and use arow
flex layout withjustify-content: space-between
to make sure the buttons each cling to their own side of the row.- we can use
flex-wrap: wrap
(and add agap
for when the buttons wrap). This will stack the buttons when the screen gets too narrow – without the need for any media queries! You can read more in this flexercise .
- we can use
QuoteForm.tsx
QuoteForm.module.css
Better; the last thing needing work is the textarea
. Here’s the styling based on the spec; I used .wrapper textarea
for the selector since CSS Modules needs something more specfic than an element such as textarea
. .wrapper textarea
means “a textarea that’s a descendant of an item with the class .wrapper”.
QuoteForm.module.css
Very nice. 😁
Controlled textarea
All right, with the styling complete, we can return to React. The textarea
needs to be controlled by React state so we can use the value in two ways:
- to know when the input is no longer empty so we can change the text of the “use random quote” button to “generate styles”
- for
buttonLabel
, I used a ternary based on the truthiness of thequote
state value
- for
- to pass along to
fetchQuoteStyles
on form submit- we’ll get to this in a minute
QuoteForm.tsx
Form submit
Let’s make the “generate styles” / “use random quote” button into a submit button, and create a handleSubmit
function for the form.
I got the type for handleSubmit
by hovering over the onSubmit
prop and seeing what VSCode had to say:
The event.preventDefault()
line in handleSubmit
keeps the form submit event from trying to submit the form to a new location. By default the new location is the current page, so if event.preventDefault()
isn’t called, the form will submit to the current page – which has the unfortunate effect of refreshing the page. See this blog post for more details.
QuoteForm.tsx
All right! At this point, both a random quote and an entered quote should work. 🎉
Start over
The last bit of functionality is the “start over” button. I created a reset
function that calls the startOver
prop and resets the quote
value to an empty string:
Congrats!
Congratulations! 🥳 You’ve finished this series on using OpenAI to style a quote. These last two workshops in particular were pretty tough. Extra congratulations if you finished all three series to create this app.
Please consider supporting my work!
Here are a few things that would really help:Let your network know about Hands-on Web Dev Challenges by posting a link and tagging me on LinkedIn or Twitter
Invite your friends to subscribe to the free Hands-on Web Dev Challenges newsletter
Tell me about your ideas for new workshops, by replying to a Substack email or posting on LinkedIn or Twitter
Become a paid subscriber to the newsletter