Creating Enrollment System without prior experience in Laravel + React over the weekend

Self Introduction

I have more than 5 years of experience in the software development, most of them are in mobile application using Flutter and Web using VueJS(Vue2 + Javascirpt), My web stack has been on silos since then. But it’s not always the frameworks that I’ve been focusing on. Most of the time its solving problems and challenges that make the most of it.

When I challenged myself to do this task over the weekend, I worked myself around 10 hours a day of reading documentation and writing code. Of course, this would be easier if this was generated by AI. but how would I debug, analyze, and fix it if I don’t know what the tool is.

TLDR; I didn’t get the job after the final interview.

I am saddened to hear this, but I know that I’ve become more confident in these challenges and I will continue to pursue and grow my career.

📍
As of this writing. I am 60% complete on my multi-platform note taking app for trekkers, hikers and campers who want to share their memories and automate their itinerary written in Flutter. Local first, it works offline, and capable of syncing to different devices.

Overview of the system

The enrollment system consist of two users.

  • The admin and user.

  • An admin can access the admin dashboard via login & register, view registered enrolees table and Add enrolee.

  • Users can register in the home page, then on submit they will receive an email that they have successfully enrolled.

Pretty hard for beginners, right?

I would have said this myself while I was a fresh graduate. But for right now, I see that this is just a minor feature to be added on a weekday job with few polishing and doing UAT and deployment.

Requirements

  • Laravel - SQLITE, ORM, Routing & Mail

  • React - HTML,CSS,JS,Typescript, Tailwind

  • InertiaJS - build pages and communicates directly to server side API

  • mailersend (optional)

  • VSCode - Code editor, allows the editor to read Language server protocol(TBD later)

Getting started

PHP Language

PHP had a reputation for inconsistent naming convention, lack of documentation, poorly written code, type safety, security and legacy code. This all changed when laravel gained popularity. My experience was seamless, but often times there are still black magic that no one can explain even the LSP itself.

I learned PHP for a single day using https://learnxinyminutes.com/php/. The language itself is comfortable knowing that there are type safe syntax and OOP fundamentals.

React + Typescript

The React framework is like a matured wild west that is widely used in big tech companies. But nowadays it has been growing rapidly and interchangeably by replacing one stack to another. Each month there are news of something that is being replaced by something new and it’s hard to keep up especially you are maintaining a big legacy code that will eventually become deprecated.

For best learning resources, head out to https://reactjs.org/ for more guides and documentation.

I was already familiar with typescript along with Javascript, though what works best is not the language but the tooling. I’m using Vite to bundle typescript code. this makes it easier to import files and allow LSP to navigate and read the files.

Mailersend (optional)

This is just for creating and sending email templates, this works well in other frameworks and we are going to use it in Laravel.

VSCode

The code editor for using LSP and using React+Laravel Extensions.

Project setup

Laravel Installation

Go to https://laravel.com/docs/12.x and run the following commands

Install PHP

MacOS

/bin/bash -c "$(curl -fsSL https://php.new/install/mac/8.4)"

Windows(Powershell)

# Run as administrator...
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://php.new/install/windows/8.4'))

Linux

/bin/bash -c "$(curl -fsSL https://php.new/install/linux/8.4)"

After running the commands, restart the terminal and install Laravel.

composer global require laravel/installer

Great! We have laravel installed in our local machine and we can run laravel in any folders

Next We’ll install nodejs

NodeJS

To run nodejs in your terminal. you must install nodejs first.

https://nodejs.org/en

To check if nodejs installed, run the following commands

node --version #v22.16.0
npm --version #v22.16.0

Let’s create a laravel project.

laravel new enrollment

We want to use react starter kit for this simple project. Select react

 ┌ Which starter kit would you like to install? ────────────────┐
 │   ○ None                                                     │
 │ › ● React                                                    │
 │   ○ Vue                                                      │
 │   ○ Livewire                                                 │
 └──────────────────────────────────────────────────────────────┘

Select Laravel’s built-in authentication

 ┌ Which authentication provider do you prefer? ────────────────┐
 │ › ● Laravel's built-in authentication                        │
 │   ○ WorkOS (Requires WorkOS account)                         │
 └──────────────────────────────────────────────────────────────┘

Select any test framework you want, but I will use Pest in this project.

 ┌ Which testing framework do you prefer? ──────────────────────┐
 │ › ● Pest                                                     │
 │   ○ PHPUnit                                                  │
 └──────────────────────────────────────────────────────────────┘

Select Yes to run npm install and npm run build

 ┌ Would you like to run npm install and npm run build? ────────┐
 │ ● Yes / ○ No                                                 │
 └──────────────────────────────────────────────────────────────┘

Once the application has been created, open the enrollment folder from VSCode

To run the laravel application.

composer run dev

This will run the project and watch any file changes. Any changes in the code will automatically update to the UI.

Important files and folders

Looking inside the laravel application, There are lots of folders files. That is an important feat because we are following the standards and architecture of the project. Soon, you will learn this convention and grow the application.

Environment(.env)

This is where the environment variables that we will monitor before we build our project. This contains sensitive information such as API keys and database connection.

Model-view-controller

Ah yes, the most used state management architecture, MVC framework.

For quick introduction, this is what MVC looks like.

flowchart TD
    subgraph User Interface
        A[User]
    end

    subgraph Controller Layer
        B[Controller]
    end

    subgraph View Layer
        C[View]
    end

    subgraph Model Layer
        D[Model]
    end

    A -->|Input| B
    B -->|Updates Model| D
    D -->|Notifies View| C
    C -->|Renders Output| A

This reads from top to bottom, left to right. In summary

  • Models are not bound in any business logic or framework logic

  • Controllers handle model changes and CRUD logic.

  • Views are there to display the output of the Controller and any specific UI logic showing checkbox state or handling error messages.

By default, Laravel will generate MVC for users since this is the basic functionality of a laravel app.

We will skip this topic and focus solely on building our own table.

Model

to create a new model, we can do it manually, or use Laravel CLI to generate our model.

The easiest way to create them is to include model, migration, factory, seeder, policy, controller, and form requests etc…


# Shortcut to generate a model, migration, factory, seeder, policy, controller, and form requests...
php artisan make:model Enrolee --all
# OR
php artisan make:model Enrolee -a

This will generate files, and configuration ready to add properties to the model.

Adding model properties

In app/Models/Enrolees.php file. We can add the following properties to our model.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;

class Enrolee extends Model
{
    /** @use HasFactory<\Database\Factories\EnroleeFactory> */
    use HasFactory, Notifiable;
    protected $table = 'enrolee';
    // Should be true or not set
    public $timestamps = true;
    protected $fillable = [
        'name',
        'birthday',
        'parent_name',
        'parent_relation',
        'parent_email',
        'student_id',
        'parent_contact_number',
    ];
}

We added HasFactory and Notifiable trait to allow us to seed fake enrolees and send emails

The $fillable getter is where we construct our object properties.

Database and Migration

💡
We are using Sqlite for our project

In the database/migrations folder, we will create a new migration.

php artisan make:migration create_enrolee_table

Inside the create_enrolee_table.php file

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('enrolee', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->string('name');
            $table->string('parent_name');
            $table->string('parent_email');
            $table->string('parent_relation');
            $table->datetime('birthday');
            $table->string('student_id')->nullable();
            $table->string('parent_contact_number')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('enrolee');
    }
};

We have created our own model and its associates. This time, we want to include it to our database migration. Next, we will create a seeder to populate our fake data

Let’s create our seeder file.

php artisan make:seeder EnroleeSeeder

Inside the /Database/Seeders/EnroleeSeeder.php file. we will create 10 fake enrolees.

<?php

namespace Database\Seeders;

use Database\Factories\EnroleeFactory;
use Illuminate\Database\Seeder;
use App\Models\Enrolee;

class EnroleeSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Enrolee::factory()->count(10)->create();
    }
}

But we can’t populate fake data in our database without creating our fake data in php. For this, we will create a factory file.

php artisan make:factory EnroleeFactory

And in the /Database/factories/EnroleeFactory.php file. we have the model and definition class.

<?php

namespace Database\Factories;

use App\Models\Enrolee;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Enrolee>
 */
class EnroleeFactory extends Factory
{


    protected $model = Enrolee::class;
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'name' =>  fake()->name(),
            'birthday' => fake()->date(),
            'parent_name' => fake()->name(),
            'parent_email' => fake()->email(),
            'parent_relation' => 'Guardian',
            'student_id' => fake()->creditCardNumber(),
            'parent_contact_number' => fake()->phoneNumber(),
        ];
    }
}

Now we can seed our fake data to database migration.

php artisan migrate:fresh --seed

Models for our React?

I asked the same question, why didn’t laravel create models for react too? InertiaJS is responsible for this feature, but I suppose there are libraries out there that generate models and form validation especially with Zod. Let’s just create a basic Enrolee model in our client side application.

Inside /resources/js/types/index.d.ts we will create our Enrolee model written in Typescript.


type EnroleeForm = {
    name: string,
    birthday: string,
    parent_name: string,
    parent_email: string,
    parent_relation: string,
    student_id: string,
    parent_contact_number: string,
}

Controllers

Let’s create our own controllers from Laravel CLI.

php artisan make:controller EnroleeController

And inside app/Http/Controllers/EnroleeController.php

<?php

namespace App\Http\Controllers;

use App\Events\EnroleeCreated;
use App\Exports\EnroleesExport;
use App\Http\Requests\UpdateEnroleeRequest;

use App\Models\Enrolee;
use App\Notifications\EnroleeSubmitNotification;
use Maatwebsite\Excel\Facades\Excel;
use Inertia\Response;

use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;

class EnroleesController extends Controller
{
    public function show(): Response
    {
        $enrolees = Enrolee::orderBy('created_at', 'desc')
            ->get([
                'student_id',
                'name',
                'birthday',
                'parent_name',
                'parent_contact_number',
                'parent_email',
                'created_at',
                'parent_relation',
            ]);

        return inertia('enrolees', [
            'enrolees' => [
                'data' => $enrolees
            ],
        ]);
    }

    /**
     * Display a listing of the resource.
     */
    public function index() {}

    /**
     * Show the form for creating a new resource.
     */
    public function create(Request $request): Response
    {
        return inertia('enrollment', [

            'status' => $request->session()->get('status'),
        ]);
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request)
    {

        $enrolee = Enrolee::create([

            'name' => $request->name,
            'birthday' => $request->birthday,
            'parent_name' => $request->parent_name,
            'parent_email' => $request->parent_email,
            'parent_relation' => $request->parent_relation,
            'student_id' => $request->student_id,
            'parent_contact_number' => $request->parent_contact_number,
        ]);
        broadcast(new EnroleeCreated($enrolee));

        $enrolee->notify(new EnroleeSubmitNotification($enrolee));

        // event(new EnroleeCreated($enrolee));

        return redirect('/enrolees');
    }

    public function submit(Request $request)
    {

        $enrolee = Enrolee::create([

            'name' => $request->name,
            'birthday' => $request->birthday,
            'parent_name' => $request->parent_name,
            'parent_email' => $request->parent_email,
            'parent_relation' => $request->parent_relation,
            'student_id' => $request->student_id,
            'parent_contact_number' => $request->parent_contact_number,
        ]);

        // event(new EnroleeCreated($enrolee));
        broadcast(new EnroleeCreated($enrolee));

        $enrolee->notify(new EnroleeSubmitNotification($enrolee));


        return back()->with('status', __('We sent a confirmation email of the enrollment form. please check your email.'));
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(Enrolee $enrolee)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateEnroleeRequest $request, Enrolee $enrolee)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Enrolee $enrolee)
    {
        $enrolee = DB::table('enrolee')->delete([
            "id" => $enrolee->id
        ]);
    }

    public function export()
    {
        return Excel::download(new EnroleesExport, 'enrolees.xlsx',);
    }
}

This will handle CRUD + Email + Export to spreadsheet file.

Mail

You can configure the mail setup here. Just pick your poison. https://laravel.com/docs/12.x/mail#main-content

Afterwards, we can create a new mail file.

php artisan make:mail EnrollmentSubmitted.php

Include these code snippet in the /app/Http/Mail/EnrollmentSubmitted.php

Be sure to edit YOUR_EMAIL_ADDRESS with the email address from mailersend.

<?php

namespace App\Mail;

use Illuminate\Mail\Mailables\Address;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use App\Models\Enrolee;
use Illuminate\Support\Arr;
use MailerSend\Helpers\Builder\Personalization;
use MailerSend\LaravelDriver\MailerSendTrait;

class EnrollmentSubmitted extends Mailable
{
    use Queueable, SerializesModels, MailerSendTrait;
    /**
     * Create a new message instance.
     */
    public function __construct(public Enrolee $enrolee) {}

    /**
     * Get the message envelope.
     */
    public function envelope(): Envelope
    {
        return new Envelope(
            subject: 'Enrollment Submitted',
            from: new Address(env('MAIL_FROM_ADDRESS', "YOUR_EMAIL_ADDRESS"), env('MAIL_FROM_NAME')),

        );
    }

    /**
     * Get the message content definition.
     */
    public function content(): Content
    {

        $to = Arr::get($this->to, '0.address');

        // Additional options for MailerSend API features
        $this->mailersend(
            template_id: "3z0vklo822147qrx",
            tags: ['laravel-notifications'],
            personalization: [
                new Personalization(
                    $to,
                    [
                        "name" => $this->enrolee->parent_name,
                        "account" => [
                            "name" => "Enrollmentify"
                        ],
                        "child_name" => $this->enrolee->name,
                        "support_email" => "YOUR_EMAIL_ADDRESS"

                    ]
                )
            ],
            precedenceBulkHeader: true,
        );

        return new Content();
    }

    /**
     * Get the attachments for the message.
     *
     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
     */
    public function attachments(): array
    {
        return [];
    }
}

Routeres

The last step for PHP is the routers. we will register this to expose the endpoints to our client side application. Modify our routes in routes/web.php

<?php

use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

use App\Http\Controllers\EnroleesController;


Route::get('/', [EnroleesController::class, 'create'])->name('home');
Route::post('/', [EnroleesController::class, 'submit'])->name('enrolees.submit');

Route::get('/new', [EnroleesController::class, 'index'])->name('new');

Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('dashboard', function () {
        return redirect('/enrolees');
    })->name('dashboard');

    Route::get('/enrolees', [EnroleesController::class, 'show']);
    Route::post('/enrolees/create', [EnroleesController::class, 'store'])->name('enrolees.create');
    Route::get('users/export/', [EnroleesController::class, 'export'])->name('enrolees.export');
});


require __DIR__ . '/settings.php';
require __DIR__ . '/auth.php';
  • The / route is for users who want to view and create form. Inside middleware.

  • The admin can export enrolees.

  • The admin can view and create enrolees.

Now that we have setup most of the php files. Let’s get started with React.

Pages

Let’s create two files in resources/js/pages folder.

enrollment.tsx

import { EnrollmentForm } from '@/components/enrollment/form'
import { SharedData } from '@/types';
import { Head, Link, usePage, } from '@inertiajs/react'
import React from 'react'

function EnrollmentPage({ status }: { status?: string }) {
    const { auth } = usePage<SharedData>().props;

    return (
        <>

            <Head title="Welcome">
                <link rel="preconnect" href="https://fonts.bunny.net" />
                <link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
            </Head>
            <div className="flex-col items-center  p-6  bg-muted ">
                <header className="mb-4 w-full text-sm not-has-[nav]:hidden ">
                    <nav className="flex items-center justify-end gap-4">
                        {auth.user ? (
                            <Link
                                href={route('dashboard')}
                                className="inline-block rounded-sm border border-[#19140035] px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#1915014a] dark:border-[#3E3E3A] dark:text-[#EDEDEC] dark:hover:border-[#62605b]"
                            >
                                Dashboard
                            </Link>
                        ) : (
                            <>
                                <Link
                                    href={route('login')}
                                    className="inline-block rounded-sm border border-transparent px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#19140035] dark:text-[#EDEDEC] dark:hover:border-[#3E3E3A]"
                                >
                                    Log in
                                </Link>
                            </>
                        )}
                    </nav>
                </header>
                <div className="flex  items-center justify-center ">
                    <div className="w-full max-w-sm md:max-w-3xl">
                        <EnrollmentForm status={status} />
                    </div>
                </div>
            </div>
        </>
    )
}

export default EnrollmentPage

enrolees.tsx


import AddEnroleeForm from '@/components/enrolees/add-enrolee-form';
import { columns, Enrolee } from '@/components/enrolees/table/columns';
import { DataTable } from '@/components/enrolees/table/data-table';
import { Button } from '@/components/ui/button';
import AppLayout from '@/layouts/app-layout';
import { type BreadcrumbItem } from '@/types';
import { Head, } from '@inertiajs/react';
import { Printer } from 'lucide-react';

const breadcrumbs: BreadcrumbItem[] = [
    {
        title: 'Enrolees',
        href: '/enrolees',
    },
];



type Props = {
    enrolees: {

        data: Enrolee[],
        current_page: number,
        next_page_url: string,
        prev_page_url: string,
        total: number,
        from: number,
        to: number,
    }

}

export default function Enrolees({ enrolees }: Props) {

    console.log(enrolees);
    // useEcho(`enrolees`, 'EnroleeCreated', (payload: Enrolee) => {
    //     console.log(payload);
    // });

    return (
        <AppLayout breadcrumbs={breadcrumbs}>
            <Head title="Enrolee" />
            <div className="flex flex-row">

                <AddEnroleeForm />
                <div className="px-2 py-4">
                    <Button className="bg-green-600" onClick={() => {

                        window.location.href = route('enrolees.export');
                    }}><Printer color='white'></Printer> Print
                    </Button>
                </div>

            </div>
            <div className='px-4 py-4'>

                <DataTable columns={columns}
                    data={enrolees.data}
                />

            </div>
        </AppLayout>
    );
}

Components

💡
The react starter kit uses radix or shadcn under the hood. by folowing the documentation in https://ui.shadcn.com/docs/

You can add as many components as you want. Here’s the list I added

We’ll create components so the pages can read those files.

Create resources/js/components/enrollment.tsx

import { cn, onPaste, validateNumber } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useForm } from "@inertiajs/react"
import { EnroleeForm } from "@/types"
import { FormEventHandler } from "react"
import InputError from "../input-error"

type EnrollmentFormProps = {
    status?: string,
} & React.ComponentProps<"div">;
export function EnrollmentForm({
    className,
    ...props
}: EnrollmentFormProps) {
    const { data, setData, post, processing, reset, errors, clearErrors } = useForm<Required<EnroleeForm>>({
        name: '',
        parent_email: 'vinceramcesoliveros@gmail.com',
        parent_relation: '',
        parent_name: '',
        parent_contact_number: '',
        birthday: new Date().toISOString().split('T')[0],
        student_id: '',
    });
    const submit: FormEventHandler = (e) => {
        e.preventDefault();

        post(route('enrolees.submit'), {
            onFinish: () => {
                console.log('From EnrollmentFormProps:', props.status);
            },
            onSuccess: () => {
                clear();
            },
            onError: () => console.error(errors)
        })

    };
    const clear = () => {
        clearErrors();
        reset();
    };

    return (
        <div className={cn("flex flex-col gap-6", className)} {...props}>
            <Card className="overflow-hidden">
                <CardContent className="grid p-0 md:grid-cols-2">
                    <form className="flex flex-col gap-4 p-6 md:p-8" onSubmit={submit}>
                        <div className="flex flex-col items-center text-center">
                            <h1 className="text-2xl font-bold">Enroll now!</h1>
                            <p className="text-balance text-muted-foreground">
                                Enrollment is on-going until school starts.
                            </p>
                        </div>
                        <div className="grid gap-4">
                            <div className="grid gap-2">
                                <Label htmlFor="student_id">LRN or Student ID</Label>
                                <Input
                                    id="student_id"
                                    required
                                    tabIndex={1}
                                    autoFocus
                                    autoComplete="student_id"
                                    value={data.student_id}
                                    onChange={(e) => setData('student_id', e.target.value)}
                                    disabled={processing}
                                    placeholder="LRN or Student ID"
                                    min={1}
                                    maxLength={10}
                                    onKeyDown={validateNumber}
                                    onPaste={onPaste}
                                />
                                <InputError message={errors.student_id} className="mt-2" />
                            </div>
                            <div className="grid gap-2">
                                <Label htmlFor="name">Enrolee Name</Label>
                                <Input
                                    id="name"
                                    type="text"
                                    required
                                    tabIndex={2}
                                    autoComplete="name"
                                    value={data.name}
                                    onChange={(e) => setData('name', e.target.value)}
                                    disabled={processing}
                                    placeholder="Full name"

                                />
                                <InputError message={errors.name} className="mt-2" />
                            </div>
                            <div className="grid gap-2">
                                <Label htmlFor="birthday">Birth date</Label>
                                <Input
                                    id="birthday"
                                    type="date"
                                    required
                                    tabIndex={3}
                                    value={data.birthday}
                                    onChange={(e) => setData('birthday', e.target.value)}
                                    disabled={processing}
                                    placeholder="Birthday"
                                />
                                <InputError message={errors.birthday} />
                            </div>

                            <div className="grid gap-2">
                                <Label htmlFor="parent_name">Parent name</Label>
                                <Input
                                    id="parent_name"
                                    type="text"
                                    required
                                    tabIndex={4}
                                    value={data.parent_name}
                                    onChange={(e) => setData('parent_name', e.target.value)}
                                    disabled={processing}
                                    placeholder="Parent name"
                                />
                                <InputError message={errors.parent_name} />
                            </div>

                            <div className="grid gap-2">
                                <Label htmlFor="parent_contact_number">Parent contact number</Label>
                                <Input
                                    id="parent_contact_number"
                                    type="text"
                                    required
                                    tabIndex={5}
                                    autoComplete="parent_contact_number"
                                    value={data.parent_contact_number}
                                    onKeyDown={validateNumber}
                                    onChange={(e) => setData('parent_contact_number', e.target.value)}
                                    disabled={processing}
                                    placeholder="+16 245 485 499"
                                />
                                <InputError message={errors.parent_contact_number} />
                            </div>

                            <div className="grid gap-2">
                                <Label htmlFor="parent_email">Parent email address</Label>
                                <Input
                                    id="parent_email"
                                    type="email"
                                    required
                                    tabIndex={6}
                                    autoComplete="parent_email"
                                    value={data.parent_email}
                                    onChange={(e) => setData('parent_email', e.target.value)}
                                    disabled={processing}
                                    placeholder="email@example.com"
                                />
                                <InputError message={errors.parent_email} />
                            </div>

                            <div className="grid gap-2">
                                <Label htmlFor="parentRelation">Parent Relationship</Label>
                                <Input
                                    id="parent_relation"
                                    type="text"
                                    required
                                    tabIndex={7}
                                    value={data.parent_relation}
                                    onChange={(e) => setData('parent_relation', e.target.value)}
                                    disabled={processing}
                                    placeholder="E.g. (Mother Father Guardian)"
                                />
                                <InputError message={errors.parent_relation} />
                            </div>

                            {props.status && <div className="mb-4 text-center text-sm font-medium text-green-600">{props.status}</div>}

                            <Button variant="default" disabled={processing} asChild>
                                <button type="submit">Submit</button>
                            </Button>
                        </div>
                    </form>
                    <div className="relative hidden md:block">
                        <img
                            src="/placeholder.jpg"
                            alt="Image"
                            className="absolute inset-0 h-full w-full object-fill px-2"
                        />
                    </div>
                </CardContent>
            </Card>
            <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary">
                By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
                and <a href="#">Privacy Policy</a>.
            </div>
        </div>
    )
}

And in resources/js/components/enrolees/add-enrolee-form.tsx


import React, { FormEventHandler } from 'react'

import { Button } from '../ui/button';
import { useForm } from '@inertiajs/react';
import { Label } from '../ui/label';
import InputError from '../input-error';
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogTitle, DialogTrigger } from '../ui/dialog';
import { Input } from '../ui/input';
import { EnroleeForm } from '@/types';
import { onPaste, validateNumber } from '@/lib/utils';

function AddEnroleeForm() {
    const { data, setData, post, processing, reset, errors, clearErrors } = useForm<Required<EnroleeForm>>({
        name: '',
        parent_email: 'vinceramcesoliveros@gmail.com',
        parent_relation: '',
        parent_name: '',
        parent_contact_number: '',
        birthday: new Date().toISOString().split('T')[0],
        student_id: '',
    });
    const submit: FormEventHandler = (e) => {
        e.preventDefault();
        post(route('enrolees.create'), {
            onSuccess: () => {
                closeModal();
            },
            onError: () => console.error(errors)
        })

    };
    const closeModal = () => {
        clearErrors();
        reset();
    };



    return (
        <Dialog >
            <DialogTrigger asChild>
                <div className="p-4">
                    <Button >Add Enrolee</Button>
                </div>
            </DialogTrigger>
            <DialogContent>
                <DialogTitle>Add enrolee</DialogTitle>
                <form className="flex flex-col gap-4" onSubmit={submit}>
                    <div className="grid gap-4">

                        <div className="grid gap-2">
                            <Label htmlFor="student_id">LRN or Student ID</Label>
                            <Input
                                id="student_id"
                                required
                                tabIndex={1}
                                autoFocus
                                autoComplete="student_id"
                                value={data.student_id}
                                onChange={(e) => setData('student_id', e.target.value)}
                                disabled={processing}
                                placeholder="LRN or Student ID"
                                min={1}
                                maxLength={10}
                                onKeyDown={validateNumber}
                                onPaste={onPaste}
                            />
                            <InputError message={errors.student_id} className="mt-2" />
                        </div>
                        <div className="grid gap-2">
                            <Label htmlFor="name">Enrolee Name</Label>
                            <Input
                                id="name"
                                type="text"
                                required
                                tabIndex={2}
                                autoComplete="name"
                                value={data.name}
                                onChange={(e) => setData('name', e.target.value)}
                                disabled={processing}
                                placeholder="Full name"

                            />
                            <InputError message={errors.name} className="mt-2" />
                        </div>
                        <div className="grid gap-2">
                            <Label htmlFor="birthday">Birth date</Label>
                            <Input
                                id="birthday"
                                type="date"
                                required
                                tabIndex={3}
                                value={data.birthday}
                                onChange={(e) => setData('birthday', e.target.value)}
                                disabled={processing}
                                placeholder="Birthday"
                            />
                            <InputError message={errors.birthday} />
                        </div>

                        <div className="grid gap-2">
                            <Label htmlFor="parent_name">Parent name</Label>
                            <Input
                                id="parent_name"
                                type="text"
                                required
                                tabIndex={4}
                                value={data.parent_name}
                                onChange={(e) => setData('parent_name', e.target.value)}
                                disabled={processing}
                                placeholder="Parent name"
                            />
                            <InputError message={errors.parent_name} />
                        </div>

                        <div className="grid gap-2">
                            <Label htmlFor="parent_contact_number">Parent contact number</Label>
                            <Input
                                id="parent_contact_number"
                                type="text"
                                required
                                tabIndex={5}
                                autoComplete="parent_contact_number"
                                value={data.parent_contact_number}
                                onKeyDown={validateNumber}
                                onChange={(e) => setData('parent_contact_number', e.target.value)}
                                disabled={processing}
                                placeholder="+16 245 485 499"
                            />
                            <InputError message={errors.parent_contact_number} />
                        </div>

                        <div className="grid gap-2">
                            <Label htmlFor="parent_email">Parent email address</Label>
                            <Input
                                id="parent_email"
                                type="email"
                                required
                                tabIndex={6}
                                autoComplete="parent_email"
                                value={data.parent_email}
                                onChange={(e) => setData('parent_email', e.target.value)}
                                disabled={processing}
                                placeholder="email@example.com"
                            />
                            <InputError message={errors.parent_email} />
                        </div>

                        <div className="grid gap-2">
                            <Label htmlFor="parentRelation">Parent Relationship</Label>
                            <Input
                                id="parent_relation"
                                type="text"
                                required
                                tabIndex={7}
                                value={data.parent_relation}
                                onChange={(e) => setData('parent_relation', e.target.value)}
                                disabled={processing}
                                placeholder="E.g. (Mother Father Guardian)"
                            />
                            <InputError message={errors.parent_relation} />
                        </div>
                        <DialogFooter className="gap-2">
                            <DialogClose asChild>
                                <Button variant="secondary" onClick={closeModal}>
                                    Cancel
                                </Button>
                            </DialogClose>
                            <DialogClose asChild>

                                <Button variant="default" disabled={processing} asChild>
                                    <button type="submit">Add enrolee</button>
                                </Button>
                            </DialogClose>
                        </DialogFooter>
                    </div>
                </form>
            </DialogContent>
        </Dialog>

    )
}

export default AddEnroleeForm

Tables

We can create tables along with columns.

resources/js/components/enrolees/table/data-table.tsx


import {
    ColumnDef,
    ColumnFiltersState,
    flexRender,
    getCoreRowModel,
    getFilteredRowModel,
    getPaginationRowModel,
    SortingState,
    useReactTable,
} from "@tanstack/react-table"

import {
    Table,
    TableBody,
    TableCell,
    TableHead,
    TableHeader,
    TableRow,
} from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { useState } from "react"
import { Input } from "@/components/ui/input"

interface DataTableProps<TData, TValue> {
    columns: ColumnDef<TData, TValue>[]
    data: TData[],
}

export function DataTable<TData, TValue>({
    columns,
    data,

}: DataTableProps<TData, TValue>) {

    const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(
        []
    );
    const [sorting] = useState<SortingState>([])

    const table = useReactTable({
        data,
        columns,
        getCoreRowModel: getCoreRowModel(),
        getPaginationRowModel: getPaginationRowModel(),
        pageCount: data.length,
        onColumnFiltersChange: setColumnFilters,
        getFilteredRowModel: getFilteredRowModel(),
        state: {
            sorting,
            columnFilters,
        },


    })

    return (
        <>

            <div className="flex items-center py-4">
                <Input
                    placeholder="Filter name..."
                    value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
                    onChange={(event) =>
                        table.getColumn("name")?.setFilterValue(event.target.value)
                    }
                    className="max-w-sm"
                />
            </div>
            <div className="rounded-md border">

                <Table>
                    <TableHeader>
                        {table.getHeaderGroups().map((headerGroup) => (
                            <TableRow key={headerGroup.id}>
                                {headerGroup.headers.map((header) => {
                                    return (
                                        <TableHead key={header.id}>
                                            {header.isPlaceholder
                                                ? null
                                                : flexRender(
                                                    header.column.columnDef.header,
                                                    header.getContext()
                                                )}
                                        </TableHead>
                                    )
                                })}
                            </TableRow>
                        ))}
                    </TableHeader>
                    <TableBody>
                        {table.getRowModel().rows?.length ? (
                            table.getRowModel().rows.map((row) => (
                                <TableRow
                                    key={row.id}
                                    data-state={row.getIsSelected() && "selected"}
                                >
                                    {row.getVisibleCells().map((cell) => (
                                        <TableCell key={cell.id}>
                                            {flexRender(cell.column.columnDef.cell, cell.getContext())}
                                        </TableCell>
                                    ))}
                                </TableRow>
                            ))
                        ) : (
                            <TableRow>
                                <TableCell colSpan={columns?.length ?? 0} className="h-24 text-center">
                                    No results.
                                </TableCell>
                            </TableRow>
                        )}
                    </TableBody>
                </Table>
            </div>

            <div className="flex items-center justify-end space-x-2 py-4">
                <div className="flex-1 text-sm text-muted-foreground">
                    {table.getFilteredSelectedRowModel().rows.length} of{" "}
                    {table.getFilteredRowModel().rows.length} row(s) selected.
                </div>
                <div className="space-x-2">
                    <Button
                        variant="outline"
                        size="sm"
                        onClick={() => table.previousPage()}
                        disabled={!table.getCanPreviousPage()}
                    >
                        Previous
                    </Button>
                    <Button
                        variant="outline"
                        size="sm"
                        onClick={() => table.nextPage()}
                        disabled={!table.getCanNextPage()}
                    >
                        Next
                    </Button>
                </div>
            </div>
        </>
    );
}

resources/js/components/enrolees/table/columns.tsx

"use client"

import { ColumnDef } from "@tanstack/react-table"

// This type is used to define the shape of our data.
// You can use a Zod schema here if you want.
export type Enrolee = {
    id: number
    name: string,
    birthday: string,
    parent_name: string,
    parent_email: string
    parent_relation: string,
}

export const columns: ColumnDef<Enrolee>[] = [
    {
        accessorKey: "student_id",
        header: "Student ID",
        maxSize: 128,
    },
    {
        accessorKey: "name",
        header: "Child name",
        maxSize: 128,
    },

    {
        accessorKey: "birthday",
        header: "Birthday",
        maxSize: 128,
        cell: (data) => {
            const val = data.getValue() as string;
            const date = new Date(val);
            return date.toLocaleDateString();
        }

    },

    {
        accessorKey: "parent_contact_number",
        header: "Parent Contact Number",
        maxSize: 128,
    },
    {
        accessorKey: "parent_name",
        header: "Parent Name",
        maxSize: 128,
    },
    {
        accessorKey: "parent_email",
        header: "Parent Email",
        maxSize: 128,
    },
    {
        accessorKey: "parent_relation",
        header: "Parent Relationship",
        maxSize: 128,
    },
]

Output

Conclusion

Although, this is not how I typically work on development. There are trial and errors I have been doing and a lot of reading documentation. The lesson for me is, hard work pays off. The sad thing is, I didn’t get hired over working this system and I hope that I can be as fast as iterating this feature, including proper documentation and architecture.

With the rise of LLMs, MCPs, Agentic coding, and other buzzwords. It would be faster for me to iterate as long as I know the tool that I’m working on.

0
Subscribe to my newsletter

Read articles from Vince Ramces Oliveros directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vince Ramces Oliveros
Vince Ramces Oliveros