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:
- React for building the UI
- Redux for managing the state of React components
- Tailwind CSS for styling the components
- Font Awesome for icons
- Vite for tooling and packaging
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.