Compare commits

..

No commits in common. "list-management" and "main" have entirely different histories.

32 changed files with 907 additions and 1508 deletions

4
.gitignore vendored
View file

@ -21,7 +21,3 @@ yarn-error.log
/.nova
/.vscode
/.zed
/storage/debugbar
sail
docker

View file

@ -4,7 +4,6 @@ namespace App\Livewire;
use App\Models\Movie;
use Livewire\Component;
use function Laravel\Prompts\select;
class MovieDetailsPanel extends Component
{
@ -15,19 +14,7 @@ class MovieDetailsPanel extends Component
public function openPanel(int $movieId): void
{
$this->selectedMovie = Movie::where('id', $movieId)
->select(
'id',
'title',
'plot',
'poster',
'director',
'year',
'actors',
'genre',
'mpaa_rating'
)
->first();
$this->selectedMovie = Movie::find($movieId);
$this->showDetails = true;
}

View file

@ -33,7 +33,7 @@ class MovieList extends Component
public function getList()
{
$list = MovieListModel::with('movies:id,poster')
$list = MovieListModel::with('movies')
->find($this->id);
if ($list) {
@ -45,12 +45,6 @@ class MovieList extends Component
}
}
public function deleteList(): void
{
$this->list->delete();
$this->redirectRoute('lists');
}
public function filterMovies(): void
{
$this->filteredMovies = collect($this->list->movies)
@ -68,7 +62,7 @@ class MovieList extends Component
$this->getList();
}
public function updatedSettingsFormIsPublic(): void
public function updatedSettingsForm(): void
{
$this->settingsForm->save();
}

View file

@ -6,33 +6,11 @@ use App\Livewire\Forms\MovieListForm;
use App\Models\MovieList;
use Exception;
use Livewire\Component;
use function Laravel\Prompts\select;
class MovieLists extends Component
{
public MovieListForm $form;
public $lists;
public $sharedLists;
public function mount()
{
$this->lists = collect();
$this->sharedLists = collect();
$this->getLists();
}
public function getLists()
{
$user = auth()->user();
if ($user) {
$this->lists = MovieList::where('user_id', $user->id)
->select("name", "id", "is_public")
->get();
} else {
$this->redirectRoute('login');
}
}
public $lists = [];
public function addList(): void
{
@ -54,8 +32,14 @@ class MovieLists extends Component
$this->form->reset();
}
public function getLists()
{
$this->lists = MovieList::all();
}
public function render()
{
$this->getLists();
return view('livewire.lists');
}
}

View file

@ -3,7 +3,6 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
@ -19,14 +18,4 @@ class MovieList extends Model
{
return $this->belongsToMany(Movie::class);
}
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function sharedUsers(): BelongsToMany
{
return $this->belongsToMany(User::class)->withPivot('permission')->withTimestamps();
}
}

View file

@ -1,10 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
//
}

View file

@ -2,13 +2,13 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;
class User extends Authenticatable
@ -44,25 +44,6 @@ class User extends Authenticatable
return $this->hasOne(UserProfile::class);
}
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
public function hasRole(string $role): bool
{
if (Context::hasHidden('roles')) {
return in_array(strtolower($role), Context::getHidden('roles'));
}
return $this->roles->contains('name', strtolower($role));
}
public function sharedLists(): BelongsToMany
{
return $this->belongsToMany(MovieList::class)->withPivot("permission")->withTimestamps();
}
/**
* Get the user's initials
*/

View file

@ -1,68 +0,0 @@
<?php
namespace App\Policies;
use App\Models\MovieList;
use App\Models\User;
class MovieListPolicy
{
/**
* Create a new policy instance.
*/
public function __construct()
{
//
}
/**
* Determine if the user can view the movie list.
*
* Grants access to the list owner and any user who has been
* granted view, edit, or admin permission.
*/
public function view(User $user, MovieList $movieList): bool
{
if ($movieList->user_id === $user->id || $movieList->is_public === true) {
return true;
}
return $movieList->sharedUsers()->where("user_id", $user->id)->exists();
}
/**
* Determine if the user can update the movie list.
*
* Grants access to the list owner and any user who has been
* granted edit or admin permission.
*/
public function update(User $user, MovieList $movieList): bool
{
if ($movieList->user_id === $user->id) {
return true;
}
return $movieList->sharedUsers()
->where("user_id", $user->id)
->whereIn("permission", ["edit", "admin"])
->exists();
}
/**
* Determine if the user can delete the movie list.
*
* Grants access to the list owner and any user who has been
* granted admin permission.
*/
public function delete(User $user, MovieList $movieList): bool
{
if ($movieList->user_id === $user->id) {
return true;
}
return $movieList->sharedUsers()
->where("user_id", $user->id)
->where("permission", "admin")
->exists();
}
}

View file

@ -4,7 +4,6 @@ namespace App\Providers;
use App\Models\Interfaces\MovieDbInterface;
use App\Services\OmdbService;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
@ -16,14 +15,6 @@ class AppServiceProvider extends ServiceProvider
public function register(): void
{
$this->app->bind(MovieDbInterface::class, OmdbService::class);
Blade::directive('role', function ($role) {
return "<?php if(auth()->user()->hasRole({$role})) : ?>";
});
Blade::directive('endrole', function ($role) {
return "<?php endif; ?>";
});
}
/**

View file

@ -19,7 +19,6 @@
"symfony/mailgun-mailer": "^7.3"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.16",
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",

1911
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -147,11 +147,11 @@ return [
// Features::registration(),
Features::resetPasswords(),
Features::emailVerification(),
// Features::twoFactorAuthentication([
// 'confirm' => true,
// 'confirmPassword' => true,
// // 'window' => 0,
// ]),
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
// 'window' => 0,
]),
],
];

View file

@ -4,7 +4,8 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
return new class extends Migration
{
/**
* Run the migrations.
*/
@ -14,7 +15,7 @@ return new class extends Migration {
$table->id();
$table->string('name');
$table->boolean('is_public')->default(false);
$table->foreignId('user_id')->constrained()->cascadeOnDelete()->index();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
}

View file

@ -1,31 +0,0 @@
<?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('movie_list_user', function (Blueprint $table) {
$table->id();
$table->foreignId("movie_list_id")->constrained()->cascadeOnDelete();
$table->foreignId("user_id")->constrained()->cascadeOnDelete();
$table->enum("permission", ["view", "edit", "admin"]);
$table->unique(["movie_list_id", "user_id"]);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('movie_list_user');
}
};

View file

@ -3,7 +3,6 @@
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
@ -17,7 +16,7 @@ class DatabaseSeeder extends Seeder
// User::factory(10)->create();
User::factory()->create([
'username' => 'testuser',
'name' => 'Test User',
'email' => 'test@example.com',
]);
}

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_','-',app()->getLocale()) }}">
<x-head/>
<body class="bg-gradient-to-br from-blue-900 via-blue-700 to-indigo-900 min-h-screen">
<body class="bg-blue-600">
<div class="container mx-auto text-white">
<x-header/>
{{ $slot }}

View file

@ -1,5 +1,5 @@
@props(['movie'])
<div class="flex flex-col bg-gray-500 w-full h-full cursor-pointer rounded-lg overflow-hidden shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300"
<div class="flex flex-col bg-gray-500 w-full h-full cursor-pointer hover:bg-gray-600 transition-colors"
wire:click="$dispatch('openMovieDetails', { movieId: {{ $movie->id }} })">
<img class="w-full h-full object-cover" src="{{$movie->poster}}" alt="{{$movie->title}}">
</div>

View file

@ -1,3 +1,3 @@
<div {{ $attributes->merge(['class' => 'bg-gray-800/90 backdrop-blur-sm p-6 flex flex-col gap-5 items-center rounded-xl shadow-xl border border-gray-700/50']) }}>
<div {{ $attributes->merge(['class' => 'bg-gray-700 p-5 flex flex-col gap-5 items-center rounded']) }}>
{{ $slot }}
</div>

View file

@ -3,7 +3,7 @@
style="mask: radial-gradient(circle at left, transparent 10px, black 11px) top left / 51% 100% no-repeat,
radial-gradient(circle at right, transparent 10px, black 11px) top right / 51% 100% no-repeat;">
<div class="p-2 border-2 border-amber-200 m-2">
<a href="/"><h1 class="font-bold font-arial text-2xl text-amber-200">Movie Night</h1></a>
<h1 class="font-bold font-arial text-2xl text-amber-200">Movie Night</h1>
</div>
</div>

View file

@ -1,5 +0,0 @@
@extends('errors::minimal')
@section('title', __('Unauthorized'))
@section('code', '401')
@section('message', __('Unauthorized'))

View file

@ -1,5 +0,0 @@
@extends('errors::minimal')
@section('title', __('Payment Required'))
@section('code', '402')
@section('message', __('Payment Required'))

View file

@ -1,5 +0,0 @@
@extends('errors::minimal')
@section('title', __('Forbidden'))
@section('code', '403')
@section('message', __($exception->getMessage() ?: 'Forbidden'))

View file

@ -1,5 +0,0 @@
@extends('errors::minimal')
@section('title', __('Not Found'))
@section('code', '404')
@section('message', __('Not Found'))

View file

@ -1,5 +0,0 @@
@extends('errors::minimal')
@section('title', __('Page Expired'))
@section('code', '419')
@section('message', __('Page Expired'))

View file

@ -1,5 +0,0 @@
@extends('errors::minimal')
@section('title', __('Too Many Requests'))
@section('code', '429')
@section('message', __('Too Many Requests'))

View file

@ -1,5 +0,0 @@
@extends('errors::minimal')
@section('title', __('Server Error'))
@section('code', '500')
@section('message', __('Server Error'))

View file

@ -1,5 +0,0 @@
@extends('errors::minimal')
@section('title', __('Service Unavailable'))
@section('code', '503')
@section('message', __('Service Unavailable'))

View file

@ -1,53 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title')</title>
<!-- Styles -->
<style>
html, body {
background-color: #fff;
color: #636b6f;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-weight: 100;
height: 100vh;
margin: 0;
}
.full-height {
height: 100vh;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center;
}
.position-ref {
position: relative;
}
.content {
text-align: center;
}
.title {
font-size: 36px;
padding: 20px;
}
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
<div class="content">
<div class="title">
@yield('message')
</div>
</div>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -24,44 +24,17 @@
</form>
</div>
<div class="w-full flex flex-col gap-5">
<h2 class="text-2xl font-bold">Your Lists</h2>
@if(!$lists->isEmpty())
<ul class="w-full flex flex-col gap-3">
@foreach($lists as $list)
<li class="flex justify-between items-center p-4 bg-gray-700/50 rounded-lg hover:bg-gray-600/50 transition-colors">
<a class="text-xl hover:text-amber-200 transition-colors" href="/lists/{{$list->id}}" wire:navigate>{{$list->name}}</a>
@if((bool)$list->is_public === true)
<i class="fa fa-earth text-green-400" title="Public" aria-label="Public"></i>
@else
<i class="fa fa-lock text-gray-400" title="Private" aria-label="Private"></i>
@endif
</li>
@endforeach
</ul>
@else
<span class="text-gray-400">No lists found.</span>
@endif
</div>
<div class="w-full flex flex-col gap-5">
<h2 class="text-2xl font-bold">Shared With You</h2>
@if(!$sharedLists->isEmpty())
<ul class="w-full flex flex-col gap-3">
@foreach($sharedLists as $list)
<li class="flex justify-between items-center p-4 bg-gray-700/50 rounded-lg hover:bg-gray-600/50 transition-colors">
<a class="text-xl hover:text-amber-200 transition-colors" href="/lists/{{$list->id}}" wire:navigate>{{$list->name}}</a>
@if((bool)$list->is_public === true)
<i class="fa fa-earth text-green-400" title="Public" aria-label="Public"></i>
@else
<i class="fa fa-lock text-gray-400" title="Private" aria-label="Private"></i>
@endif
</li>
@endforeach
</ul>
@else
<span class="text-gray-400">No lists found.</span>
@endif
</div>
<ul class="w-full flex flex-col gap-5">
@foreach($lists as $list)
<li class="flex py-5 justify-between text-center">
<a class="font-bold text-2xl" href="/lists/{{$list->id}}" wire:navigate>{{$list->name}}</a>
@if((bool)$list->is_public === true)
<i class="fa fa-earth" title="Public" aria-label="Public"></i>
@else
<i class="fa fa-lock" title="Private" aria-label="Private"></i>
@endif
</li>
@endforeach
</ul>
</x-ui.card>
</div>

View file

@ -1,21 +1,17 @@
<div class=" text-white pt-10 flex flex-col gap-5">
<div class="flex flex-row justify-between items-center mx-2 sm:mx-0">
<h1 class="text-2xl sm:text-3xl font-bold">{{$list->name}}</h1>
@can('delete', $list)
<button type="button" wire:click="toggleSettings"
class="hover:bg-blue-600 cursor-pointer text-white px-4 py-2 rounded">
<i class="fas fa-cog text-2xl"></i>
</button>
@endcan
</div>
<div>
<livewire:search-panel :list-id="$list->id"/>
<livewire:movie-details-panel/>
<button type="button" wire:click="toggleSettings"
class="hover:bg-blue-600 cursor-pointer text-white px-4 py-2 rounded">
<i class="fas fa-cog text-2xl"></i>
</button>
</div>
<x-ui.card class="overflow-hidden min-h-screen">
<div class="absolute">
<livewire:search-panel :list-id="$list->id"/>
<livewire:movie-details-panel/>
</div>
<div class="relative w-full overflow-hidden">
<div class="flex transition-transform duration-300 ease-in-out"
@ -25,24 +21,21 @@
<div class="w-full flex-shrink-0 flex flex-col gap-5">
<div class="flex flex-col-reverse sm:flex-row gap-5 sm:gap-0 justify-between w-full">
<div>
<input class="flex bg-white p-2 rounded w-full sm:w-100" type="text"
placeholder="Filter movies"
<input class="flex bg-white p-2 rounded sm:w-100" type="text" placeholder="Filter movies"
wire:model.live="filterText"
wire:keyup="filterMovies"/>
</div>
@can("update", $list)
<hr class="my-2 sm:my-0"/>
<button wire:click="$dispatch('openSearch')"
class="bg-green-500 text-white p-2 rounded">
Add Movie
</button>
@endcan
</div>
@if(!$filteredMovies->isEmpty())
<ul class="grid grid-cols-2 sm:grid-cols-4 gap-5">
@foreach ($filteredMovies as $movie)
<li>
<li class="bg-gray-200">
<x-movie :movie="$movie"/>
</li>
@endforeach
@ -63,69 +56,23 @@
<h2 class="text-xl font-semibold">Settings</h2>
</div>
<div class="flex flex-col gap-2 w-full p-5 bg-gray-700">
<label for="list-name" class="font-bold">List Name</label>
<div class="flex flex-col gap-2 w-full p-5">
<label for="list-name">List Name</label>
<div class="flex flex-row">
<input type="text" wire:model.live="settingsForm.name" id="list-name"
class="w-full p-2 rounded rounded-r-none bg-white"/>
<button class="bg-green-500 p-2 rounded-r" type="submit" wire:click="saveSettings">Save
<button class="bg-green-400 p-2 rounded-r" type="submit" wire:click="saveSettings">Save
</button>
</div>
</div>
<div
class="flex items-center justify-between bg-gray-700 hover:opacity-85 p-5 rounded">
<label for="is_public" class="text-white font-bold cursor-pointer">Make list public</label>
class="flex items-center justify-between bg-gray-700 hover:bg-gray-500 hover:opacity-85 p-5 rounded">
<label for="is_public" class="text-white cursor-pointer">Make list public</label>
<input type="checkbox"
id="is_public"
wire:model.live="settingsForm.isPublic"
class="w-5 h-5 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500">
</div>
<div class="p-5 flex flex-col gap-5 bg-gray-700 rounded">
<span class="font-bold">Collaborators</span>
<details class="hover:cursor-pointer bg-gray-600 p-5 rounded">
<ul class="flex flex-col gap-2 py-2">
<li><span class="font-bold">Viewer</span>: Can view the list, but cannot make any changes.</li>
<li><span class="font-bold">Editor</span>: Can add/remove movies from the list.</li>
<li><span class="font-bold">Admin</span>: Can make any changes to the list including deleting it. Can also invite other users to collaborate on this list.</li>
</ul>
</details>
<ul>
<li class="flex justify-between ">
<span>Bob</span>
<select>
<option value="view">Viewer</option>
<option value="edit">Editor</option>
<option value="admin">Admin</option>
</select>
</li>
</ul>
</div>
<div class="p-5 flex flex-col gap-3 bg-gray-700 rounded">
<span class="font-bold">Invite collaborators</span>
<span class="hover:cursor-pointer">Enter a comma separated list of emails.</span>
<textarea class="bg-white rounded text-black p-2" placeholder="user1@example.com, user2@example.com, user3@example.com"></textarea>
<button type="button" class="p-2 rounded bg-green-500">Send Invites</button>
</div>
@can('delete', $list)
<div
class="flex items-center justify-between bg-gray-700 p-5 rounded">
<label for="delete_list" class="text-white cursor-pointer">Delete List</label>
<button name="delete_list"
type="button"
class="bg-red-500 p-2 rounded font-bold"
wire:click="deleteList"
wire:confirm="Are you sure you want to delete this list?"
>
Delete List
</button>
</div>
@endcan
</div>
</div>
</div>

View file

@ -0,0 +1,5 @@
<x-layouts.app>
<div class="text-white text-2xl">
<livewire:movie-lists/>
</div>
</x-layouts.app>