Saturday, March 15, 2025
Building a Contact Management App with Laravel 12 and React
Posted by
Building a Contact Management App with Laravel 12 and React
This guide will walk you through creating a contact management application using Laravel 12 with React, Inertia.js, TypeScript, and Tailwind CSS. The app will allow users to create contact groups and add contacts to those groups.
Requirements
- Install PHP, Apache and MySQL Via XAMPP
- First uninstall the existing one to make sure you have the latest PHP installed
- Install a fresh XAMPP
- Configure XAMPP so that Apache and MySQL start automatically on Windows start
-
Install Composer
-
Install the Laravel installer
- First remove any existing installer if you have it
composer global remove laravel/installer
- Add a fresh Laravel installer which supports interactivity
composer global require laravel/installer
Create a new Laravel app Using the installer
- Use the Laravel installer
laravel new
Then it will start asking you questions, answer accordingly:
- Select "Inertia with React" as the frontend stack
- run the app
cd your-app-name
npm install && npm run build && npm run dev
- In a new terminal tab:
php artisan serve
Project Requirements
- Laravel 12 with Inertia.js and React starter kit (already installed)
- User authentication with role management (user, admin)
- Group management (with pop-up modal form)
- Contact management (with dedicated page form)
Table of Contents
- Database Setup
- User Model Customization
- Creating the Group Feature
- Creating the Contact Feature
- Frontend Implementation with React & TypeScript
- Form Implementation
- UI Enhancements
- Testing
- Deployment
1. Database Setup
Configure Database Connection Using Mysql
Update your .env
file with your database credentials:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=contacts_app
DB_USERNAME=root
DB_PASSWORD=
Configure Database Connection Using Postgress
You need to create a free account on render and also then also install postgress PgAdmin optionally Step 3: Enable PHP PostgreSQL Extensions Make sure these extensions are enabled in your php.ini file:
extension=php_pdo_pgsql.dll
extension=php_pgsql.dll
update ur .env
DB_CONNECTION=pgsql
DB_HOST=
DB_PORT=5432
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
DB_URL =
You may need to add SSL settings to your config/database.php file:
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'require',
]
Create Migrations
# Create migration for adding role and phone to users table
php artisan make:migration add_role_and_phone_to_users_table --table=users
# Create migration for groups
php artisan make:migration create_groups_table
# Create migration for contacts
php artisan make:migration create_contacts_table
Edit the migration files:
database/migrations/xxxx_xx_xx_add_role_and_phone_to_users_table.php
:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->enum('role', ['user', 'admin'])->default('user');
$table->string('phone')->nullable();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['role', 'phone']);
});
}
};
database/migrations/xxxx_xx_xx_create_groups_table.php
:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('groups', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('groups');
}
};
database/migrations/xxxx_xx_xx_create_contacts_table.php
:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('contacts', function (Blueprint $table) {
$table->id();
$table->foreignId('group_id')->constrained()->onDelete('cascade');
$table->string('name');
$table->string('phone');
$table->string('email')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('contacts');
}
};
Run the migrations:
php artisan migrate
2. User Model Customization
Update the User model to include the new fields:
php artisan make:enum UserRole
Edit app/Enums/UserRole.php
:
<?php
namespace App\Enums;
enum UserRole: string
{
case USER = 'user';
case ADMIN = 'admin';
}
Update the User model (app/Models/User.php
):
<?php
namespace App\Models;
use App\Enums\UserRole;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
'role',
'phone',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'role' => UserRole::class,
];
/**
* Get the groups for the user.
*/
public function groups(): HasMany
{
return $this->hasMany(Group::class);
}
/**
* Check if the user is an admin.
*/
public function isAdmin(): bool
{
return $this->role === UserRole::ADMIN;
}
}
3. Creating the Group Feature
Create the Model and Controller
# Create Group model with controller
php artisan make:model Group -c -r
Edit the Group
model (app/Models/Group.php
):
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
class Group extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'user_id',
'name',
'slug',
];
/**
* Boot the model.
*/
protected static function boot()
{
parent::boot();
static::creating(function ($group) {
$group->slug = Str::slug($group->name);
});
static::updating(function ($group) {
$group->slug = Str::slug($group->name);
});
}
/**
* Get the user that owns the group.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the contacts for the group.
*/
public function contacts(): HasMany
{
return $this->hasMany(Contact::class);
}
}
Edit the GroupController (app/Http/Controllers/GroupController.php
):
<?php
namespace App\Http\Controllers;
use App\Models\Group;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
use Inertia\Response;
class GroupController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): Response
{
$groups = Auth::user()->groups()->withCount('contacts')->get();
return Inertia::render('Groups/Index', [
'groups' => $groups,
]);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
]);
Auth::user()->groups()->create($validated);
return redirect()->route('groups.index')->with('success', 'Group created successfully.');
}
/**
* Display the specified resource.
*/
public function show(Group $group): Response
{
$this->authorize('view', $group);
$group->load('contacts');
return Inertia::render('Groups/Show', [
'group' => $group,
]);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Group $group): RedirectResponse
{
$this->authorize('update', $group);
$validated = $request->validate([
'name' => 'required|string|max:255',
]);
$group->update($validated);
return redirect()->route('groups.index')->with('success', 'Group updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Group $group): RedirectResponse
{
$this->authorize('delete', $group);
$group->delete();
return redirect()->route('groups.index')->with('success', 'Group deleted successfully.');
}
}
Create a Policy for Group
php artisan make:policy GroupPolicy --model=Group
Edit app/Policies/GroupPolicy.php
:
<?php
namespace App\Policies;
use App\Enums\UserRole;
use App\Models\Group;
use App\Models\User;
class GroupPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Group $group): bool
{
return $user->id === $group->user_id || $user->role === UserRole::ADMIN;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return true;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Group $group): bool
{
return $user->id === $group->user_id || $user->role === UserRole::ADMIN;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Group $group): bool
{
return $user->id === $group->user_id || $user->role === UserRole::ADMIN;
}
}
Update Routes
Edit routes/web.php
:
<?php
use App\Http\Controllers\ContactController;
use App\Http\Controllers\GroupController;
use App\Http\Controllers\ProfileController;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return Inertia::render('Welcome', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'laravelVersion' => Application::VERSION,
'phpVersion' => PHP_VERSION,
]);
});
Route::get('/dashboard', function () {
return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
// Group Routes
Route::resource('groups', GroupController::class);
// Contact Routes
Route::resource('groups.contacts', ContactController::class)->shallow();
});
require __DIR__.'/auth.php';
4. Creating the Contact Feature
Create the Model and Controller
# Create Contact model with controller
php artisan make:model Contact -c -r
Edit the Contact
model (app/Models/Contact.php
):
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Contact extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'group_id',
'name',
'phone',
'email',
];
/**
* Get the group that owns the contact.
*/
public function group(): BelongsTo
{
return $this->belongsTo(Group::class);
}
}
Edit the ContactController (app/Http/Controllers/ContactController.php
):
<?php
namespace App\Http\Controllers;
use App\Models\Contact;
use App\Models\Group;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class ContactController extends Controller
{
/**
* Show the form for creating a new resource.
*/
public function create(Group $group): Response
{
$this->authorize('view', $group);
return Inertia::render('Contacts/Create', [
'group' => $group,
]);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request, Group $group): RedirectResponse
{
$this->authorize('view', $group);
$validated = $request->validate([
'name' => 'required|string|max:255',
'phone' => 'required|string|max:20',
'email' => 'nullable|email|max:255',
]);
$group->contacts()->create($validated);
return redirect()->route('groups.show', $group)
->with('success', 'Contact created successfully.');
}
/**
* Display the specified resource.
*/
public function show(Contact $contact): Response
{
$this->authorize('view', $contact);
$contact->load('group');
return Inertia::render('Contacts/Show', [
'contact' => $contact,
]);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Contact $contact): Response
{
$this->authorize('update', $contact);
$contact->load('group');
return Inertia::render('Contacts/Edit', [
'contact' => $contact,
]);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Contact $contact): RedirectResponse
{
$this->authorize('update', $contact);
$validated = $request->validate([
'name' => 'required|string|max:255',
'phone' => 'required|string|max:20',
'email' => 'nullable|email|max:255',
]);
$contact->update($validated);
return redirect()->route('groups.show', $contact->group)
->with('success', 'Contact updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Contact $contact): RedirectResponse
{
$this->authorize('delete', $contact);
$group = $contact->group;
$contact->delete();
return redirect()->route('groups.show', $group)
->with('success', 'Contact deleted successfully.');
}
}
Create a Policy for Contact
php artisan make:policy ContactPolicy --model=Contact
Edit app/Policies/ContactPolicy.php
:
<?php
namespace App\Policies;
use App\Enums\UserRole;
use App\Models\Contact;
use App\Models\User;
class ContactPolicy
{
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Contact $contact): bool
{
return $user->id === $contact->group->user_id || $user->role === UserRole::ADMIN;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Contact $contact): bool
{
return $user->id === $contact->group->user_id || $user->role === UserRole::ADMIN;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Contact $contact): bool
{
return $user->id === $contact->group->user_id || $user->role === UserRole::ADMIN;
}
}
5. Frontend Implementation with React & TypeScript
Create TypeScript interfaces for your data models. Create a file resources/js/types/index.ts
:
export interface User {
id: number;
name: string;
email: string;
phone?: string;
role: "user" | "admin";
email_verified_at?: string;
}
export interface Group {
id: number;
user_id: number;
name: string;
slug: string;
contacts_count?: number;
contacts?: Contact[];
created_at: string;
updated_at: string;
}
export interface Contact {
id: number;
group_id: number;
name: string;
phone: string;
email?: string;
created_at: string;
updated_at: string;
group?: Group;
}
export interface PageProps {
auth: {
user: User;
};
flash: {
success?: string;
error?: string;
};
}
6. Form Implementation
Create a Group Modal Component
Create a file resources/js/Components/GroupModal.tsx
:
import { Fragment, useEffect } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { useForm } from "@inertiajs/react";
import InputError from "@/Components/InputError";
import InputLabel from "@/Components/InputLabel";
import TextInput from "@/Components/TextInput";
import PrimaryButton from "@/Components/PrimaryButton";
import SecondaryButton from "@/Components/SecondaryButton";
import { Group } from "@/types";
interface GroupModalProps {
show: boolean;
onClose: () => void;
group?: Group | null;
}
export default function GroupModal({
show,
onClose,
group = null,
}: GroupModalProps) {
const { data, setData, post, put, processing, errors, reset } = useForm({
name: "",
});
useEffect(() => {
if (show) {
setData("name", group ? group.name : "");
} else {
reset();
}
}, [show, group]);
const submit = (e: React.FormEvent) => {
e.preventDefault();
if (group) {
put(route("groups.update", group.id), {
onSuccess: () => onClose(),
});
} else {
post(route("groups.store"), {
onSuccess: () => onClose(),
});
}
};
return (
<Transition show={show} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div>
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
{group ? "Edit Group" : "Create New Group"}
</Dialog.Title>
<div className="mt-2">
<form onSubmit={submit} className="space-y-6">
<div>
<InputLabel htmlFor="name" value="Name" />
<TextInput
id="name"
className="mt-1 block w-full"
value={data.name}
onChange={(e) => setData("name", e.target.value)}
required
autoFocus
/>
<InputError message={errors.name} className="mt-2" />
</div>
<div className="mt-6 flex justify-end space-x-2">
<SecondaryButton onClick={onClose}>
Cancel
</SecondaryButton>
<PrimaryButton type="submit" disabled={processing}>
{group ? "Update" : "Create"}
</PrimaryButton>
</div>
</form>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
);
}
Create Groups Index Page
Create a file resources/js/Pages/Groups/Index.tsx
:
import { useState } from "react";
import { Head, Link } from "@inertiajs/react";
import { PageProps, Group } from "@/types";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import GroupModal from "@/Components/GroupModal";
import PrimaryButton from "@/Components/PrimaryButton";
import DangerButton from "@/Components/DangerButton";
import Modal from "@/Components/Modal";
interface Props extends PageProps {
groups: Group[];
}
export default function Index({ auth, groups, flash }: Props) {
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingGroup, setEditingGroup] = useState<Group | null>(null);
const [groupToDelete, setGroupToDelete] = useState<Group | null>(null);
return (
<AuthenticatedLayout
user={auth.user}
header={
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
Groups
</h2>
}
>
<Head title="Groups" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
{flash.success && (
<div className="mb-4 bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
{flash.success}
</div>
)}
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div className="p-6 text-gray-900">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-medium">Your Contact Groups</h3>
<PrimaryButton onClick={() => setShowCreateModal(true)}>
Create New Group
</PrimaryButton>
</div>
{groups.length === 0 ? (
<p className="text-gray-500">
You don't have any contact groups yet. Create one to get
started!
</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{groups.map((group) => (
<div
key={group.id}
className="border rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow"
>
<div className="flex justify-between items-start">
<Link href={route("groups.show", group.id)}>
<h4 className="text-lg font-semibold text-blue-600 hover:text-blue-800">
{group.name}
</h4>
</Link>
<div className="flex space-x-2">
<button
onClick={() => setEditingGroup(group)}
className="text-gray-500 hover:text-gray-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</button>
<button
onClick={() => setGroupToDelete(group)}
className="text-red-500 hover:text-red-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
</div>
<p className="text-gray-600 mt-2">
{group.contacts_count || 0} contacts
</p>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
<GroupModal
show={showCreateModal}
onClose={() => setShowCreateModal(false)}
/>
<GroupModal
show={!!editingGroup}
onClose={() => setEditingGroup(null)}
group={editingGroup}
/>
<Modal show={!!groupToDelete} onClose={() => setGroupToDelete(null)}>
<div className="p-6">
<h2 className="text-lg font-medium text-gray-900">
Are you sure you want to delete this group?
</h2>
<p className="mt-1 text-sm text-gray-600">
This will also delete all contacts in this group. This action cannot
be undone.
</p>
<div className="mt-6 flex justify-end space-x-2">
<PrimaryButton onClick={() => setGroupToDelete(null)}>
Cancel
</PrimaryButton>
<DangerButton
onClick={() => {
if (groupToDelete) {
window.location.href = route(
"groups.destroy",
groupToDelete.id
);
}
setGroupToDelete(null);
}}
>
Delete
</DangerButton>
</div>
</div>
</Modal>
</AuthenticatedLayout>
);
}
Create Group Show Page
Create a file resources/js/Pages/Groups/Show.tsx
:
import { Head, Link, router } from "@inertiajs/react";
import { PageProps, Group, Contact } from "@/types";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import PrimaryButton from "@/Components/PrimaryButton";
interface Props extends PageProps {
group: Group & { contacts: Contact[] };
}
export default function Show({ auth, group, flash }: Props) {
const deleteContact = (contactId: number) => {
if (confirm("Are you sure you want to delete this contact?")) {
router.delete(route("contacts.destroy", contactId));
}
};
return (
<AuthenticatedLayout
user={auth.user}
header={
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
{group.name}
</h2>
}
>
<Head title={group.name} />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
{flash.success && (
<div className="mb-4 bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
{flash.success}
</div>
)}
<div className="mb-4 flex justify-between items-center">
<Link
href={route("groups.index")}
className="text-blue-600 hover:text-blue-800 flex items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 mr-1"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
Back to Groups
</Link>
<Link href={route("groups.contacts.create", group.id)}>
<PrimaryButton>Add New Contact</PrimaryButton>
</Link>
</div>
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div className="p-6 text-gray-900">
<h3 className="text-lg font-medium mb-4">
Contacts in {group.name}
</h3>
{group.contacts.length === 0 ? (
<p className="text-gray-500">
No contacts in this group yet. Add one to get started!
</p>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Name
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Phone
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Email
</th>
<th
scope="col"
className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{group.contacts.map((contact) => (
<tr key={contact.id}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{contact.name}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-500">
{contact.phone}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-500">
{contact.email}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex justify-end space-x-2">
<Link
href={route("contacts.edit", contact.id)}
className="text-indigo-600 hover:text-indigo-900"
>
Edit
</Link>
<button
onClick={() => deleteContact(contact.id)}
className="text-red-600 hover:text-red-900"
>
Delete
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}