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.
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.
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
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.
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
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.
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
