Courses

[NEW] NativePHP: Build Mobile App with Laravel

Intro and Web/Mobile App Structure

In this course, we will build a mobile application to view the events and register as attendees for the event or specific talk(s). Here's a quick video demonstration.

So yes, this mobile app is built with Laravel 12 (and Livewire Starter Kit), not with "traditional" mobile tools like React Native or Flutter.

This is possible with NativePHP. In short, it allows you to transform a regular web application into a mobile app using its responsive mobile design.


But First... Web Application and API

In this case, we're dealing with such a scenario: imagine, you already have a web site for events/talks, working with the DB on the same server.

Then, we create the API for that website that deals with the same database.

Finally, we will create a separate mobile client with that remote API. Our mobile client will be a Laravel application.


The Plan of Lessons: Web App + API + Mobile

Before we dive into the NativePHP and mobile client, I want to explain the initial structure of our database and Laravel web application, so you can understand the whole story.

This first lesson will be about this web application. In the upcoming lessons 2 and 3, we will create the API for it and then dive into a mobile client with NativePHP.

Here's a quick demo of the initial website:

In our web application, users will be able to:

  • View and attend events
  • View and attend specific talks from those events

Also, administrators can manage the events, but since it won't be a feature in the mobile app, we will not discuss that admin area; you can check its code in the repository.


Events and Talks: DB Structure

To understand the following lessons about API and mobile, you must familiarize yourself with the database.

Here's the DB structure:

Now, more details in Laravel Migrations and Eloquent Models:

Migration

Schema::create('events', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('description');
$table->dateTime('start_datetime');
$table->dateTime('end_datetime');
$table->string('location');
$table->timestamps();
});

Then, we should allow our users to join the Event. For that, we'll use a many-to-many table:

Migration

Schema::create('event_user', function (Blueprint $table) {
$table->id();
$table->foreignId('event_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->boolean('is_attending')->default(false);
$table->timestamps();
 
$table->unique(['event_id', 'user_id']);
});

Finally, we need talks to be associated with our Event:

Migration

Schema::create('talks', function (Blueprint $table) {
$table->id();
$table->foreignId('event_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('description');
$table->string('speaker_name');
$table->dateTime('start_time');
$table->dateTime('end_time');
$table->timestamps();
});

With a Model that looks like this:

app/Models/Talk.php

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class Talk extends Model
{
use HasFactory;
 
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'event_id',
'title',
'description',
'speaker_name',
'start_time',
'end_time',
];
 
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'start_time' => 'datetime',
'end_time' => 'datetime',
];
}
 
public function event(): BelongsTo
{
return $this->belongsTo(Event::class);
}
 
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)
->withPivot('is_attending')
->withTimestamps();
}
 
public function attendees(): BelongsToMany
{
return $this->belongsToMany(User::class)
->wherePivot('is_attending', true)
->withTimestamps();
}
}

Last, is our Event Model that includes all the relationships we will use:

app/Models/Event.php

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Event extends Model
{
use HasFactory;
 
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'user_id',
'title',
'description',
'start_datetime',
'end_datetime',
'location',
];
 
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'start_datetime' => 'datetime',
'end_datetime' => 'datetime',
];
}
 
public function owner(): BelongsTo
{
return $this->belongsTo(User::class);
}
 
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)
->withPivot('is_attending')
->withTimestamps();
}
 
public function attendees(): BelongsToMany
{
return $this->belongsToMany(User::class)
->wherePivot('is_attending', true)
->withTimestamps();
}
 
public function talks(): HasMany
{
return $this->hasMany(Talk::class);
}
}

With this model, we can query:

  • All users within the Event (including is_attending flag)
  • Only those who are attending

Event Controller

Now, let's build a simple Event Controller for showing the events:

app/Http/Controllers/EventController.php

use App\Models\Event;
use App\Models\Talk;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
 
class EventController extends Controller
{
public function index(Request $request)
{
$query = Event::query();
 
// Handle search
if ($request->filled('search')) {
$search = $request->input('search');
$query->where(function ($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%")
->orWhere('location', 'like', "%{$search}%");
});
}
 
$events = $query->orderBy('start_datetime', 'asc')->get();
 
return view('events.index', [
'events' => $events,
'search' => $request->input('search', ''),
]);
}
 
// ... I'm skipping admin functions create/edit/update
// Check them in the repository
 
public function show(Event $event)
{
$talks = $event->talks()
->orderBy('start_time', 'asc')
->get()
->groupBy(function ($talk) {
return $talk->start_time->format('Y-m-d');
});
 
$isAttending = Auth::user()->isAttendingEvent($event);
 
// Add attendance status to each talk
$talks->transform(function ($dayTalks) {
return $dayTalks->map(function ($talk) {
$talk->isAttending = Auth::user()->isAttendingTalk($talk);
return $talk;
});
});
 
return view('events.show', [
'event' => $event,
'talks' => $talks,
'isAttending' => $isAttending,
]);
}
}

Blade Views

Let's add our Index view:

resources/views/events/index.blade.php

<x-layouts.app>
 
<div class="mb-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-800 dark:text-gray-100">{{ __('Events') }}</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">{{ __('Browse all upcoming events') }}</p>
</div>
<a href="{{ route('events.create') }}"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors duration-200">
{{ __('Create Event') }}
</a>
</div>
</div>
 
<!-- Search Form -->
<div class="mb-6">
<form method="GET" action="{{ route('events.index') }}" class="flex gap-4">
<div class="flex-1">
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<input type="text" name="search" value="{{ $search }}"
placeholder="Search events by title, description, or location..."
class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md leading-5 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:placeholder-gray-400 dark:focus:placeholder-gray-300 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-400 dark:focus:border-blue-400">
</div>
</div>
<button type="submit"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors duration-200">
Search
</button>
@if($search)
<a href="{{ route('events.index') }}"
class="px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white text-sm font-medium rounded-md transition-colors duration-200">
Clear
</a>
@endif
</form>
</div>
 
@if($events->isEmpty())
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-8 text-center">
<svg class="w-16 h-16 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z">
</path>
</svg>
<h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 mb-2">
@if($search)
No events found for "{{ $search }}"
@else
No events found
@endif
</h3>
<p class="text-gray-500 dark:text-gray-400">
@if($search)
Try adjusting your search terms or browse all events.
@else
There are no events scheduled at the moment.
@endif
</p>
</div>
@else
@if($search)
<div class="mb-4">
<p class="text-sm text-gray-600 dark:text-gray-400">
Found {{ $events->count() }} {{ Str::plural('event', $events->count()) }} for "{{ $search }}"
</p>
</div>
@endif
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($events as $event)
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow duration-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-100">{{ $event->title }}</h3>
<span
class="px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 rounded-full">
{{ $event->start_datetime->format('M d') }}
</span>
</div>
 
<p class="text-gray-600 dark:text-gray-400 text-sm mb-4 line-clamp-3">
{{ $event->description }}
</p>
 
<div class="space-y-3 mb-4">
<div class="flex items-center text-sm text-gray-500 dark:text-gray-400">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z">
</path>
</svg>
{{ $event->start_datetime->format('g:i A') }} - {{ $event->end_datetime->format('g:i A') }}
</div>
 
<div class="flex items-center text-sm text-gray-500 dark:text-gray-400">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
{{ $event->location }}
</div>
 
@if($event->talks->count() > 0)
<div class="flex items-center text-sm text-gray-500 dark:text-gray-400">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10">
</path>
</svg>
{{ $event->talks->count() }} {{ Str::plural('talk', $event->talks->count()) }}
</div>
@endif
</div>
 
<div class="flex items-center justify-between">
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ $event->attendees->count() }} {{ Str::plural('attendee', $event->attendees->count()) }}
</div>
 
<a href="{{ route('events.show', $event) }}"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors duration-200">
{{ __('View Details') }}
</a>
</div>
</div>
@endforeach
</div>
@endif
 
</x-layouts.app>

And a Show view:

resources/views/events/show.blade.php

<x-layouts.app>
 
<!-- Hero Section -->
<div class="relative bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-800 dark:to-gray-900 rounded-2xl p-8 mb-8 overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-r from-blue-600/10 to-purple-600/10"></div>
<div class="relative z-10">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 mb-4">
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"></path>
</svg>
Event Details
</div>
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-3 leading-tight">{{ $event->title }}</h1>
<p class="text-lg text-gray-700 dark:text-gray-300 mb-6 leading-relaxed max-w-3xl">{{ $event->description }}</p>
 
<div class="flex items-center text-gray-600 dark:text-gray-400 mb-4">
<svg class="w-5 h-5 mr-3 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span class="font-medium">{{ $event->location }}</span>
</div>
 
<!-- Attend Event Button -->
<div class="flex items-center space-x-4">
<form method="POST" action="{{ route('events.attend', $event) }}" class="inline">
@csrf
@if($isAttending)
<button type="submit" class="px-8 py-4 bg-gradient-to-r from-red-600 to-pink-600 hover:from-red-700 hover:to-pink-700 text-white font-bold rounded-xl shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 flex items-center text-lg">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
Cancel Attendance
</button>
@else
<button type="submit" class="px-8 py-4 bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white font-bold rounded-xl shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 flex items-center text-lg">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Attend Event
</button>
@endif
</form>
 
@if($isAttending)
<div class="flex items-center px-4 py-2 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 rounded-lg">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="font-medium">You're attending!</span>
</div>
@endif
 
@if($event->user_id === auth()->id())
<a
href="{{ route('events.edit', $event) }}"
class="px-6 py-4 bg-gradient-to-r from-gray-600 to-gray-700 hover:from-gray-700 hover:to-gray-800 text-white font-bold rounded-xl shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 flex items-center text-lg"
>
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
Edit Event
</a>
@endif
</div>
</div>
 
<div class="text-right ml-8">
<div class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-xl p-6 shadow-lg">
<div class="text-2xl font-bold text-gray-900 dark:text-white mb-1">
{{ $event->start_datetime->format('M d') }}
</div>
<div class="text-sm text-gray-600 dark:text-gray-400 mb-3">
{{ $event->start_datetime->format('Y') }}
</div>
<div class="flex items-center justify-center text-sm font-medium text-blue-600 dark:text-blue-400">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
{{ $event->start_datetime->format('g:i A') }} - {{ $event->end_datetime->format('g:i A') }}
</div>
</div>
</div>
</div>
</div>
</div>
 
<!-- Schedule Section -->
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-100 dark:border-gray-700 overflow-hidden">
<div class="bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-700 dark:to-gray-800 px-8 py-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center">
<div class="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center mr-4">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
</div>
<div>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Event Schedule</h2>
<p class="text-gray-600 dark:text-gray-400">Complete agenda and speaker lineup</p>
</div>
</div>
</div>
 
<div class="p-8">
@if($talks->isEmpty())
<div class="text-center py-16">
<div class="w-20 h-20 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center mx-auto mb-6">
<svg class="w-10 h-10 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
</div>
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">No talks scheduled yet</h3>
<p class="text-gray-600 dark:text-gray-400 max-w-md mx-auto">The event schedule will be updated soon. Check back later for the complete agenda.</p>
</div>
@else
@foreach($talks as $date => $dayTalks)
<div class="mb-12 last:mb-0">
<div class="flex items-center mb-6">
<div class="w-1 h-8 bg-gradient-to-b from-blue-500 to-purple-600 rounded-full mr-4"></div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white">
{{ \Carbon\Carbon::parse($date)->format('l, F d, Y') }}
</h3>
</div>
 
<div class="space-y-6">
@foreach($dayTalks as $index => $talk)
<div class="group relative">
<!-- Timeline connector -->
@if($index < count($dayTalks) - 1)
<div class="absolute left-6 top-12 w-0.5 h-16 bg-gradient-to-b from-blue-200 to-transparent"></div>
@endif
 
<div class="relative bg-white dark:bg-gray-700 rounded-xl p-6 shadow-lg border border-gray-100 dark:border-gray-600 hover:shadow-xl transition-all duration-300 group-hover:scale-[1.02]">
<!-- Timeline dot -->
<div class="absolute left-0 top-6 w-3 h-3 bg-blue-600 rounded-full border-4 border-white dark:border-gray-800 shadow-lg transform -translate-x-1.5"></div>
 
<div class="ml-8">
<div class="flex items-start justify-between mb-4">
<div class="flex-1">
<h4 class="text-xl font-bold text-gray-900 dark:text-white mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{{ $talk->title }}
</h4>
 
<div class="flex items-center space-x-4 mb-3">
<div class="flex items-center text-sm font-medium text-blue-600 dark:text-blue-400">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
{{ $talk->start_time->format('g:i A') }} - {{ $talk->end_time->format('g:i A') }}
</div>
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
{{ $talk->speaker_name }}
</div>
</div>
 
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
{{ $talk->description }}
</p>
</div>
 
<div class="ml-6">
<form method="POST" action="{{ route('talks.attend', $talk) }}" class="inline">
@csrf
@if($talk->isAttending)
<button type="submit" class="px-6 py-3 bg-gradient-to-r from-red-600 to-pink-600 hover:from-red-700 hover:to-pink-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200 flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
{{ __('Cancel') }}
</button>
@else
<button type="submit" class="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200 flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
{{ __('Attend') }}
</button>
@endif
</form>
 
@if($talk->isAttending)
<div class="mt-2 flex items-center px-3 py-1 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 rounded-lg text-sm">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="font-medium">Attending</span>
</div>
@endif
</div>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
@endforeach
@endif
</div>
</div>
 
</x-layouts.app>


Joining Event/Talk

Next, we need to work on a way for our Users to join an event:

To do this, we have added a few more methods to our EventController:

app/Http/Controllers/EventController.php

// ...
 
public function toggleAttendance(Event $event)
{
$user = Auth::user();
 
if (!$user) {
return redirect()->route('login');
}
 
$existingAttendance = $event->users()
->where('user_id', $user->id)
->first();
 
if ($existingAttendance) {
$event->users()->updateExistingPivot($user->id, [
'is_attending' => !$existingAttendance->pivot->is_attending
]);
 
$message = $existingAttendance->pivot->is_attending
? 'You are no longer attending this event.'
: 'You are now attending this event!';
} else {
// Add new attendance
$event->users()->attach($user->id, ['is_attending' => true]);
$message = 'You are now attending this event!';
}
 
return redirect()->back()->with('success', $message);
}
 
public function toggleTalkAttendance(Talk $talk)
{
$user = Auth::user();
 
if (!$user) {
return redirect()->route('login');
}
 
$existingAttendance = $talk->users()
->where('user_id', $user->id)
->first();
 
if ($existingAttendance) {
$talk->users()->updateExistingPivot($user->id, [
'is_attending' => !$existingAttendance->pivot->is_attending
]);
 
$message = $existingAttendance->pivot->is_attending
? 'You are no longer attending this talk.'
: 'You are now attending this talk!';
} else {
// Add new attendance
$talk->users()->attach($user->id, ['is_attending' => true]);
$message = 'You are now attending this talk!';
}
 
return redirect()->back()->with('success', $message);
}

And our Routes:

routes/web.php

use App\Http\Controllers\Settings;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\EventController;
use Illuminate\Support\Facades\Route;
 
Route::get('/', function () {
return view('welcome');
})->name('home');
 
Route::middleware(['auth'])->group(function () {
Route::get('dashboard', DashboardController::class)->name('dashboard');
Route::get('events', [EventController::class, 'index'])->name('events.index');
Route::get('events/create', [EventController::class, 'create'])->name('events.create');
Route::post('events', [EventController::class, 'store'])->name('events.store');
Route::get('events/{event}/edit', [EventController::class, 'edit'])->name('events.edit');
Route::put('events/{event}', [EventController::class, 'update'])->name('events.update');
Route::get('events/{event}', [EventController::class, 'show'])->name('events.show');
Route::post('events/{event}/attend', [EventController::class, 'toggleAttendance'])->name('events.attend');
Route::post('talks/{talk}/attend', [EventController::class, 'toggleTalkAttendance'])->name('talks.attend');
 
Route::get('settings/profile', [Settings\ProfileController::class, 'edit'])->name('settings.profile.edit');
Route::put('settings/profile', [Settings\ProfileController::class, 'update'])->name('settings.profile.update');
Route::delete('settings/profile', [Settings\ProfileController::class, 'destroy'])->name('settings.profile.destroy');
Route::get('settings/password', [Settings\PasswordController::class, 'edit'])->name('settings.password.edit');
Route::put('settings/password', [Settings\PasswordController::class, 'update'])->name('settings.password.update');
Route::get('settings/appearance', [Settings\AppearanceController::class, 'edit'])->name('settings.appearance.edit');
});
 
require __DIR__ . '/auth.php';

From there, we have a few buttons on the Event Detail page.

One for Event attendance:

resources/views/events/show.blade.php

{{-- ... --}}
 
<!-- Attend Event Button -->
<div class="flex items-center space-x-4">
<form method="POST" action="{{ route('events.attend', $event) }}" class="inline">
@csrf
@if($isAttending)
<button type="submit" class="px-8 py-4 bg-gradient-to-r from-red-600 to-pink-600 hover:from-red-700 hover:to-pink-700 text-white font-bold rounded-xl shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 flex items-center text-lg">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
Cancel Attendance
</button>
@else
<button type="submit" class="px-8 py-4 bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white font-bold rounded-xl shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 flex items-center text-lg">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Attend Event
</button>
@endif
</form>
 
@if($isAttending)
<div class="flex items-center px-4 py-2 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 rounded-lg">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="font-medium">You're attending!</span>
</div>
@endif
 
{{-- ... --}}

And of course, the same type of button for our Talks:

resources/views/events/show.blade.php

{{-- ... --}}
 
<div class="ml-6">
<form method="POST" action="{{ route('talks.attend', $talk) }}" class="inline">
@csrf
@if($talk->isAttending)
<button type="submit" class="px-6 py-3 bg-gradient-to-r from-red-600 to-pink-600 hover:from-red-700 hover:to-pink-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200 flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
{{ __('Cancel') }}
</button>
@else
<button type="submit" class="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200 flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
{{ __('Attend') }}
</button>
@endif
</form>
 
@if($talk->isAttending)
<div class="mt-2 flex items-center px-3 py-1 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 rounded-lg text-sm">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="font-medium">Attending</span>
</div>
@endif
</div>
 
{{-- ... --}}

That's it for our base application — it is working fine and usable. But imagine we want to give our Users a better experience by giving them a mobile app.

In the next lesson, we will add the API to our web application, which will later be consumed by a NativePHP-powered Laravel mobile client.


You can find the whole repository of this initial web application on our GitHub Repository - Event System API and check what we are starting with.

avatar

Assalaam o Alaikum, It is great, however with that if only breeze was installed, the person who is a complete beginner can also go step by step through the whole process

avatar

Not sure I get what you are trying to say.

Are you a beginner in NativePHP? Or Laravel itself?

avatar

i am a beginner in native php yes, but not in laravel.

What I am trying to say is your tutorials are amazing, and adds a lot of value in experience.

Your tutorials sometime lack a step or two that makes it a bit hard to follow along. Like breeze needed to be installed but it was not mentioned anywhere in this chapter. And some of the routes were not really working as the controllers were not defined.

I could take a jump and change it as I have experience, but it gets difficult for an absolute beginner.

I really respect your work, learned a lot from your youtube channel and that is why I am giving feedback, after all with feedback,many things are improved, right?

avatar

Dont get me wrong here, it wasnt a blame or anything - just trying to understand :)

But if you feel that some parts are missing (and it is totally possible that they are!) - just let us know directly and we will gladly update the tutorial!

After all, the feedback helps us improve ❤