read24 - Admin UI - Managing Classrooms (Part 2)

August 10, 2020

Roger Ngo

Most of the backend changes for adding basic classroom management functions are now done (for now). It is not time to create some pages in the Admin UI to make use of these new API routes.‌

The following pages will need to be created:

  1. Add classroom
  2. Edit a classroom
  3. List a classroom

Deleting a classroom will be created later. There are a few things that must happen before I do that. For example, having the ability to associate a teacher, or a student to a classroom.

The add, and edit classroom pages will be pretty simple. The forms will just have 2 TextField components to allow data entry for name, and slug. These pages will be called AddClassroom.tsx, and EditClassroom.tsx.

Before I begin any work, I’ll take this opportunity to organize the folder structure a little bit for the admin panel. Each page will now reside in their own folder. So, the folder structure now looks like:

pages
-- AddBook
---- AddBook.css
---- AddBook.tsx
-- EditBook
---- EditBook.css
---- EditBook.tsx
-- ListBooks
---- ListBooks.css
---- ListBooks.tsx
-- Home
---- Home.tsx

AddClassroom Page

Let’s create an AddClassroom folder, and the page itself. As like all the other pages I have created, it is just a simple stubbed component which renders Hello.

export default function AddClassroom() {
    return (
        <div className="add-classroom container">
            Hello
        </div>
    );
}

AddClassroom will have 2 fields and a button. the button will submit the data by calling the POST /admin/classroom endpoint with the request body.‌

The form itself will have 3 state variables:‌

  1. the name value
  2. the slug value
  3. the type of alert which should be appearing (0 for none, 1 for success, 2 for error)
const [name, setName] = useState('');
const [slug, setSlug] = useState('');
const [alertState, setAlertState] = useState({
    message: '',
    type: 0
});

Now there should be event handlers declared to handle all of the name, and slug field changes:

function onNameChange(e: SyntheticEvent) {
    const value = e.target as HTMLInputElement;
    
    setName(value.value);
}

function onSlugChange(e: SyntheticEvent) {
    const value = e.target as HTMLInputElement;

    setSlug(value.value);
}

This is all very straightforward so far.‌

The submitForm handler does most of the heavy lifting for this form. It will call the POST request, and configure rendering of the alert banner by setting the alert banner state to either display a success message, or an error message.

async function submitForm() {
    const response = await (await fetch(`${API_HOST}/admin/classroom`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({name, slug})
    })).json();

    if (response.message) {
        setAlertState({
            type: AlertBannerType.Error,
            message: `Could not add the classroom. Reason: ${response.message}`
        });     
    }   else {
        setAlertState({
            type: AlertBannerType.Success,
            message: 'Successfully added the classroom.'
        });
    } 
}

Finally to wrap this page all up, it is as easy as writing JSX:

return (
    <div className="add-classroom container">
        <div className="row">
            <div className="col col-12">
                <h2>Add Classroom</h2>
            </div>
        </div>
        <div className="row">
            <div className="col col-6">
                <TextField id="add-classroom-name" label="Name" value={name} onChange={onNameChange} />
            </div>
        </div>
        <div className="row">
            <div className="col col-6">
                <TextField id="add-classroom-slug" label="Slug" value={slug} onChange={onSlugChange} />
            </div>
        </div>
        <div className="row">
            <div className="col col-12">
                {
                    alertState.type 
                        ? <AlertBanner message={alertState.message} type={alertState.type} visible={true} /> 
                        : undefined
                }
            </div>
        </div>
        <div className="row">
            <div className="col col-2">
                <button type="button" className="btn btn-primary" onClick={submitForm}>Submit</button>
            </div>
        </div>
    </div>
);

ListClassroom Page‌

Great, now it is time to create a ListClassroom page. This is the same as the ListBooks page, and because of this the table should be very familiar:

Class Name Slug Actions
Classroom Name slugName [Edit]‌

I will use the GET /admin/classroom/all/page/:page endpoint to retrieve the collection of classrooms. ‌

Notice that this table can be paginated. Just like ListBooks, I will need a pagination bar to flip through pages. This is my chance to refactor the bar that was created in ListBooks into its own component, PaginationBar.

import React from 'react';
import './PaginationBar.css';

interface PaginationBarProps {
    totalPages: number;
    currentPage: number;
    onClick: (page: number) => void;
}

export default function PaginationBar(props: PaginationBarProps) {
    const {
        currentPage,
        totalPages,
        onClick
    } = props;

    const pageLinks = [];

    if (currentPage - 1 >= 1)
        pageLinks.push(<button className='page-nav-link'
            key={'arrow-back'}
            onClick={() => onClick(currentPage - 1)}
        >{'<<'}</button>);

    for(let i = 1; i <= totalPages; i++) {
        pageLinks.push(<button className={`page-nav-link ${i === currentPage ? 'active' : undefined}`}
            key={i} 
            onClick={() => onClick(i)}
        >{i}</button>);
    }

    if(currentPage + 1 <= totalPages)
        pageLinks.push(<button className='page-nav-link'
            key={'arrow-next'}
            onClick={() => onClick(currentPage + 1)}
        >{'>>'}</button>);

    return (
        <div className="pagination-bar row justify-content-center">
            <div className="col col-4">
                {pageLinks}
            </div>
        </div>
    );
}

Then I can just render it by calling it like this:

<PaginationBar totalPages={totalPages} currentPage={page} onClick={setPage} />

Now for the ListClassroom page itself, it is just to fetch data and display it in a table. For now, the Edit button will not work since there is no edit page just yet.

import React, { useEffect, useState } from 'react';
import { API_HOST } from '../../common/constants';
import PaginationBar from '../../components/PaginationBar';
import { Link } from 'react-router-dom';

export default function ListClassrooms() {
    const [data, setData] = useState([]);
    const [totalPages, setTotalPages] = useState(0);
    const [page, setPage] = useState(1);

    useEffect(() => {
        const getData = async () => {
            const data = await (await fetch(`${API_HOST}/admin/classroom/all/page/${page}`)).json();

            setData(data.classrooms);
            setTotalPages(data._meta.pages);
        };

        getData();
    }, [page]);

    return (
        <div className="list-classrooms container">
            <div className="row">
                <div className="col col-12">
                    <h2>List Classrooms</h2>
                </div>
            </div>
            <div className="row">
                <div className="col col-12">
                    <table className="table">
                        <thead>
                            <tr>
                                <th>Name</th>
                                <th>Slug</th>
                                <th>Actions</th>
                            </tr>
                        </thead>
                        <tbody>
                            {
                                data.map((c: any) =>
                                    <tr key={c.id}>
                                        <td>{c.name}</td>
                                        <td>{c.slug}</td>
                                        <td>
                                            <Link to={'/'} className="btn btn-primary">Edit</Link>
                                        </td>
                                    </tr>
                                )
                            }
                        </tbody>
                    </table>
                </div>
            </div>
            <PaginationBar totalPages={totalPages} currentPage={page} onClick={setPage} />
        </div>
    );
}

Now the page will look like this:

EditClassroom Page

Ok now it is time to create the EditClassroom page so that the Edit button will actually do something.

It is more or less the same as AddClassroom except that it will be assumed that an ID route parameter is passed. This ID can be used to perform a GET request to obtain the classroom information.

useEffect(() => {
    setId((props.match.params as any).id);
}, []);

useEffect(() => {
    const fetchedClassroom = async () => {
        const data = await (await fetch(`${API_HOST}/admin/classroom/${id}`)).json();
        
        setName(data.name);
        setSlug(data.slug);
    };

    fetchedClassroom();
}, [id]);

Then of course, the ListClassroom page needs to be updated to take the user to the EditClassroom page with the proper ID when building the table.

<Link to={`/classroom/edit/${c.id}`} className="btn btn-primary">Edit</Link>

Now, I can navigate to the EditClassroom page to edit a classroom!