Random Quote Machine

Intro

This is the first project in the freeCodeCamp Front End Development Libraries Certification. The objective is to build a website that shows a random quote every time the user presses the ‘New Quote’ button. The suggested method is to use a front end library like React. Read more about the project at Build a Random Quote Machine

Check out the end product at https://quote-machine.projects.yasakdogra.com

Stack

Most of my front end projects will be built with:

Check out how to setup the project in another one of my posts: React with Redux, Tailwind and More

NOTE: I have set up my own quotes API to fetch random quotes. You can read a bit about it at Hono with MongoDB. This is, however, not required because this is a front end only project. We can just use an API provider like API Ninjas

Quote State

State slice

This is a very short project. We only need to fetch a random quote on button press and update the state and then Redux and React will work to update the UI. We can put all the code for the quote state in one file.

Create a new file src/state/quoteSlice.ts and import redux types and functions we will use

import { PayloadAction, createAsyncThunk, createSlice } from "@reduxjs/toolkit"

Create constants for quotes API URL and background colors. The colors can be in a format supported by CSS background style

const QUOTES_API_URL = 'https://quotesapi.projects.yasakdogra.com';
const COLORS = ['navy', 'teal', 'orange', '#99B080', 'purple', 'maroon', '#0766AD'];

Create an interface for the shape of our state

interface QuoteState {
    text: string;
    author: string;
    color: string;
}

Set initial state. This will be the state our application loads with

interface QuoteState {
    text: string;
    author: string;
    color: string;
}

Create an async thunk for fetching quotes using createAsyncThunk. This will let us add fetchQuote.pending, fetchQuote.fulfilled and fetchQuote.rejected case handlers to manipulate our state. We also export it so we can use it anywhere in our application

export const fetchQuote = createAsyncThunk('quote/fetchQuote', async () => {
    const response = await fetch(QUOTES_API_URL);
    if(!response.ok) {
        const message = `An error has occured: ${response.status}`;
        throw new Error(message);
    }
    return response.json();
});

Now we will use the createSlice function to initialize the state and generate the action and action types. We will set the name of the state slice, initial state and add the case for fetchQuote.fulfilled

const quoteSlice = createSlice({
    name: 'quote',
    initialState,
    reducers: {
    },
    extraReducers (builder) {
        builder.addCase(fetchQuote.fulfilled, (state, action: PayloadAction<{text: string, author: string}>) => {
            const oldColorId = COLORS.indexOf(state.color);
            let newColorId;
            do {
                newColorId = Math.floor(Math.random() * COLORS.length);
            } while(oldColorId === newColorId);
            state.color = COLORS[newColorId];
            state.text = action.payload.text;
            state.author = action.payload.author;
        });
    }
});

Export the quote reducer so we can add it to our state store

export default quoteSlice.reducer;

State store

Now that we have the quote state slice, we can setup the state store.

Create a new file src/state/store.ts. Create the state store using configureStore and add the exported quote reducer to it. Also, create and export RootState and AppDispatch types so we can get type hints in our application when we use the state and dispatch actions

import { configureStore } from '@reduxjs/toolkit';
import quoteReducer from './quoteSlice';

export const store = configureStore({
    reducer: {
        quote: quoteReducer,
    },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Quote component

We will create a stateless component and pass three props to it: quote text, quote author and quote color. These props can be updated using fetchQuote in our App component.

Create a new file **src/components/Quote.tsx **. Import the resources we need to use, like the Font Awesome icons

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faQuoteLeft } from "@fortawesome/free-solid-svg-icons";

Next we create the Quote component with the necessary props. Inside the return statement of the Quote component we can write the HTML we want to render. We can create two divs: one for text and another for author. The text will have a quote icon on the left. We can style them using the color prop and Tailwind classes.

type QuoteProps = {
    text: string;
    author: string;
    color: string;
};

function Quote({ text, author, color }: QuoteProps) {
    return (
        <>
            <div id="quote-text" className="flex items-baseline">
                <FontAwesomeIcon
                    icon={faQuoteLeft}
                    size="2xl"
                    style={{ color: color, opacity: 0.5 }}
                />
                <p
                    id="text"
                    className="p-2 ml-2 text-4xl"
                    style={{ color: color }}
                >
                    {text}
                </p>
            </div>
            <div id="quote-author" className="overflow-hidden">
                <p
                    id="author"
                    className="pr-4 float-right italic"
                    style={{ color: color }}
                >
                    - {author}
                </p>
            </div>
        </>
    );
}

export default Quote;

App component

The project already has a src/App.tsx file with the App component. We just need to modify the code in this file.

Let’s start by removing unused imports and bring in the ones we need

import { useDispatch, useSelector } from 'react-redux'
import { AppDispatch, RootState } from './state/store'
import { fetchQuote } from './state/quoteSlice'
import Quote from './components/Quote'

Inside the App component we can get access to state with useSelector

const color = useSelector((state: RootState) => state.quote.color)
const text = useSelector((state: RootState) => state.quote.text)
const author = useSelector((state: RootState) => state.quote.author)

We can dispatch actions with useDispatch

const dispatch = useDispatch<AppDispatch>();
dispatch(fetchQuote());

Let’s put everything together and add the Quote component to the App component. We also add two buttons, one to tweet the link and the other to dispatch fetchQuote action which will asynchronously fetch a new quote and update the UI when the promise is fulfilled

import './App.css'
import { useDispatch, useSelector } from 'react-redux'
import { AppDispatch, RootState } from './state/store'
import { fetchQuote } from './state/quoteSlice'
import Quote from './components/Quote'


function App() {
    const color = useSelector((state: RootState) => state.quote.color)
    const text = useSelector((state: RootState) => state.quote.text)
    const author = useSelector((state: RootState) => state.quote.author)

    const dispatch = useDispatch<AppDispatch>()

    const newQuote = () => {
        dispatch(fetchQuote())
    }

    return (
        <>
        <div
          className="min-h-screen w-screen flex justify-center items-center"
          style={{ backgroundColor: color }}
        >
          <div
            id="quote-box"
            className="max-w-lg p-4 w-128 bg-white border border-gray-200 rounded-md shadow"
          >
            <Quote text={text} author={author} color={color} />
            <div className="overflow-hidden pt-4">
              <a
                id="tweet-quote"
                href={'https://twitter.com/intent/tweet?text="' + text + '" ' + author}
                className="p-2 rounded-md active:opacity-60 float-left"
                style={{ backgroundColor: color, color: 'white' }}
              >Tweet</a>
              <button
                id="new-quote"
                className="p-2 rounded-md active:opacity-60 float-right"
                style={{ backgroundColor: color, color: 'white' }}
                onClick={newQuote}
              >
                New Quote
              </button>
            </div>
          </div>
        </div>
      </>
    )
}

export default App

Thank you for reading. You can also check out my other projects for this series below.