@RemiM I had tried your suggestion and still found it wasn't sharing the state. So, I started over on my own trying to read through the Livewire docs to figure this out. I have something so far that will trigger the watch state (albeit a bit slower than yours I think) and it syncs it with the db, calling Livewire.
watch-input.blade.php (Livewire)
<div
x-data="{
isWatched: $wire.entangle('isWatched').defer}"
x-on:watch-toggled.window="isWatched = $event.detail.isWatched"
>
<label class="block text-sm text-primary-400 mb-1">{{ $isWatched ? 'Watched' : 'Watch' }}</label>
<div class="relative flex items-center group">
<div class="cursor-pointer" x-on:click="$wire.toggleWatch">
<x-icon-eye-outline
class="w-10 h-10 hover:text-primary-400 transition-colors {{
$isWatched ? 'fill-primary-500 stroke-current text-zinc-950' : 'fill-none text-primary-500'
}}"/>
</div>
</div>
</div>
WatchInput.php
<?php
namespace App\Livewire;
use App\Models\Movie;
use Illuminate\View\View;
use Livewire\Attributes\Validate;
use Livewire\Component;
class WatchInput extends Component
{
#[Validate('required|boolean')]
public bool $isWatched = false;
public int $movieId;
public function mount(int $movieId): void
{
$this->movieId = $movieId;
$movie = Movie::findOrFail($movieId);
// If there's a watch record for this user, grab its "is_watched" value; otherwise default to false.
$this->isWatched = (bool) ($movie->watches()
->where('user_id', auth()->id())
->value('is_watched') ?? false
);
}
public function toggleWatch(): void
{
$this->validate();
// Toggle the state
$this->isWatched = !$this->isWatched;
// Do a minimal update on the watchlist relationship
$movie = Movie::findOrFail($this->movieId);
$movie->watches()->updateOrCreate(
['user_id' => auth()->id()],
['is_watched' => $this->isWatched]
);
$this->dispatch('watch-toggled', $this->movieId, $this->isWatched);
}
public function render(): View
{
return view('livewire.watch-input');
}
}
show.blade.php (/movies/show) (Non-Livewire)
@php
use Carbon\Carbon;
@endphp
<x-app>
<x-slot:title>
{{ $movie->title }} - ReelTrack
</x-slot:title>
{{-- Flash Message Container --}}
@if(session('status'))
<div class="absolute bg-green-600 rounded-md p-4 top-10 bottom-10 left-10 right-10">
<div class="alert">
{{ session('status') }}
</div>
</div>
@endif
@if(session('error'))
<div class="absolute bg-red-600 rounded-md p-4 top-50 bottom-0 left-0 right-0 z-10">
<div class="alert">
{{ session('error') }}
</div>
</div>
@endif
{{-- Backdrop with gradient overlay --}}
<div class="relative aspect-[2.76/1] w-full">
<div class="absolute inset-0 bg-linear-to-t from-zinc-900 "></div>
<img
src="{{ $movie->backdrop_path }}"
alt="{{ $movie->title }}"
class="w-full h-full object-cover object-center"
/>
</div>
{{-- Main content --}}
<div class="container max-w-6xl mx-auto -mt-64 relative z-10">
<div class="flex gap-8">
{{-- Left column: Poster and actions --}}
<div class="w-[300px] shrink-0" x-data="{ isOpen: false }">
<img
src="{{ $movie->poster_path }}"
alt="{{ $movie->poster_path }}"
class="w-full rounded-lg shadow-lg"
/>
{{-- Action buttons --}}
@auth
<div
class="mt-4 space-y-2"
>
<button
type="button"
class="w-full bg-primary-500 text-zinc-800 py-2 rounded-md hover:bg-primary-600 transition-colors"
>
Add to Watchlist
</button>
<button
type="button"
@click="isOpen = true"
class="w-full bg-zinc-800 text-primary-500 py-2 rounded-md hover:bg-zinc-700 transition-colors"
>
Leave a Reel or Review
</button>
{{-- Modal --}}
<x-review-modal :movie="$movie"/>
</div>
@endauth
</div>
{{-- Right column: Movie details --}}
<div class="grow ">
{{-- Title and tagline --}}
<h1 class="text-4xl text-primary-500 font-bold">{{ $movie->title }}</h1>
@if (!empty($movie->tagline))
<p class="text-xl italic mt-2 text-primary-300">{{ $movie->tagline }}</p>
@endif
{{-- Meta information --}}
<div class="flex items-center gap-4 mt-4">
<span class="text-zinc-400">{{ $movie->release_date->format('F d, Y') }}</span>
<span class="text-zinc-400">{{ $movie->runtime }} min</span>
</div>
{{-- Genres --}}
@if (!empty($movie->genres))
<div class="flex gap-2 mt-4">
@foreach ($movie->genres as $genre)
<a
href="{{ route('movies.index', ['genre' => $genre->name ?? 'genre-placeholder']) }}"
class="px-3 py-1 font-medium rounded-full border border-primary-500 text-sm
hover:bg-primary-500 hover:text-white transition-colors"
>
{{ $genre->name }}
</a>
@endforeach
</div>
@endif
{{-- Movie Stats --}}
@include('movies.partials.movie-stats', ['movie' => $movie])
{{-- Overview --}}
<div class="mt-8">
<h2 class="text-primary-500 font-semibold mb-2">Overview</h2>
<p class="leading-relaxed">{{ $movie->overview }}</p>
</div>
{{-- User Actions Row --}}
<!-- TODO: All of these components need to talk to each other in the modal -->
@if (Auth::check())
<form>
@csrf
<div class="flex items-center space-x-10 my-4">
<!-- TODO: Make one parent component that holds all 3? -->
{{-- Watched Date --}}
<livewire:watch-input :movie-id="$movie->id"/>
{{-- Rating Input --}}
<livewire:rating-input :movie="$movie"/>
{{-- Like Input --}}
<livewire:like-input :movie="$movie"/>
{{-- Watchlist --}}
{{-- Kebab case for the prop is okay --}}
<livewire:watchlist-input :movie-id="$movie->id"/>
</div>
</form>
@else
<!-- TODO: Find a way to redirect back here -->
<div class="my-6"><a href="{{ route('login') }}"
class="p-2 rounded-md bg-primary-700 text-zinc-200 shadow-md
inset-shadow-sm hover:bg-primary-800 transition-all ">Login
to rate or review</a></div>
@endauth
{{-- Cast & Crew Tabs --}}
<x-cast-crew-tabs :cast="$movie->cast" :crew="$movie->crew"/>
<a href="{{ route('movies.cast-and-crew', $movie) }}" class="block mt-4
text-primary-500 font-medium
hover:text-primary-600
hover:underline
transition-all underline-offset-2">Full
Cast & Crew</a>
{{-- Reviews --}}
{{-- <div class="mt-8" id="movie_reviews">--}}
{{-- <x-movie-reviews :movie="$movie"/>--}}
{{-- </div>--}}
</div>
</div>
</div>
</x-app>
review-modal.blade.php (Non-Livewire)
<div>
<!-- Modal backdrop -->
<div
class="fixed inset-0 bg-zinc-800/50 backdrop-blur-lg z-50"
x-show="isOpen"
x-cloak
x-transition
@click="isOpen = false"
@keydown.escape.window="isOpen = false"
style="display: none;" {{-- Ensures Alpine begins with display: none --}}
>
<!-- Modal inner container -->
<div
class="fixed inset-0 flex items-center justify-center p-4"
@click.stop {{-- Prevent clicks inside from closing the modal --}}
>
{{-- Modal content --}}
<div class="bg-zinc-900 rounded-lg shadow-xl max-w-2xl w-full p-6">
{{-- Modal Header --}}
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold text-primary-500">
Review {{ $movie->title }}
</h2>
<button
type="button"
@click="isOpen = false;confirm('Are you sure you want to cancel?')"
class="text-zinc-400 hover:text-primary-500"
>
<!-- Close Icon -->
<x-icon-close class="w-6 h-6"/>
</button>
</div>
{{-- Begin form --}}
<form
action="{{ route('movies.reel.store', ['movie' => $movie->id]) }}"
method="POST"
class="space-y-6"
>
@csrf
{{-- Rating and Like Row --}}
<div class="flex items-center gap-8">
{{-- Rating --}}
<div class="flex-1">
{{-- Rating Input --}}
<livewire:rating-input :movie="$movie"/>
</div>
{{-- Like --}}
<div class="flex-1">
<livewire:like-input :movie="$movie"/>
</div>
</div>
{{-- Watch --}}
<livewire:watch-input :movie-id="$movie->id"/>
{{-- Review Text --}}
<div>
<label class="block text-sm text-primary-400 mb-1">Your Review</label>
<textarea
name="content"
rows="4"
placeholder="Share your thoughts about the movie..."
class="w-full bg-zinc-800 text-primary-400 border border-zinc-700 rounded-md px-3 py-2
placeholder:text-zinc-500 focus:ring-primary-500 focus:border-primary-500"
>{{ old('content') }}</textarea>
</div>
{{-- Submit Button --}}
<div class="flex justify-end">
<button
type="submit"
class="px-4 py-2 bg-primary-500 text-white rounded-md hover:bg-primary-600 transition-colors"
>
Submit
</button>
</div>
</form>
</div>
</div>
</div>
</div>
I think I'm close. For some reason the store just wasn't working. I would check the DOM and nothing was being stored. The script wouldn't change at all.