diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 78c8763..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "enabledMcpjsonServers": [ - "laravel-boost" - ], - "enableAllProjectMcpServers": true -} diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 3e09433..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,241 +0,0 @@ - -=== foundation rules === - -# Laravel Boost Guidelines - -The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications. - -## Foundational Context - -This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. - -- php - 8.4.17 -- laravel/framework (LARAVEL) - v12 -- laravel/prompts (PROMPTS) - v0 -- laravel/sanctum (SANCTUM) - v4 -- laravel/boost (BOOST) - v2 -- laravel/mcp (MCP) - v0 -- laravel/pail (PAIL) - v1 -- laravel/pint (PINT) - v1 -- laravel/sail (SAIL) - v1 -- phpunit/phpunit (PHPUNIT) - v11 -- tailwindcss (TAILWINDCSS) - v4 - -## Conventions - -- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. -- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. -- Check for existing components to reuse before writing a new one. - -## Verification Scripts - -- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important. - -## Application Structure & Architecture - -- Stick to existing directory structure; don't create new base folders without approval. -- Do not change the application's dependencies without approval. - -## Frontend Bundling - -- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. - -## Documentation Files - -- You must only create documentation files if explicitly requested by the user. - -## Replies - -- Be concise in your explanations - focus on what's important rather than explaining obvious details. - -=== boost rules === - -# Laravel Boost - -- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. - -## Artisan - -- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters. - -## URLs - -- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port. - -## Tinker / Debugging - -- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. -- Use the `database-query` tool when you only need to read from the database. -- Use the `database-schema` tool to inspect table structure before writing migrations or models. - -## Reading Browser Logs With the `browser-logs` Tool - -- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. -- Only recent browser logs will be useful - ignore old logs. - -## Searching Documentation (Critically Important) - -- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. -- Search the documentation before making code changes to ensure we are taking the correct approach. -- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first. -- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. - -### Available Search Syntax - -1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'. -2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit". -3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order. -4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit". -5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms. - -=== php rules === - -# PHP - -- Always use curly braces for control structures, even for single-line bodies. - -## Constructors - -- Use PHP 8 constructor property promotion in `__construct()`. - - `public function __construct(public GitHub $github) { }` -- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private. - -## Type Declarations - -- Always use explicit return type declarations for methods and functions. -- Use appropriate PHP type hints for method parameters. - - -```php -protected function isAccessible(User $user, ?string $path = null): bool -{ - ... -} -``` - -## Enums - -- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. - -## Comments - -- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex. - -## PHPDoc Blocks - -- Add useful array shape type definitions when appropriate. - -=== laravel/core rules === - -# Do Things the Laravel Way - -- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. -- If you're creating a generic PHP class, use `php artisan make:class`. -- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. - -## Database - -- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. -- Use Eloquent models and relationships before suggesting raw database queries. -- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. -- Generate code that prevents N+1 query problems by using eager loading. -- Use Laravel's query builder for very complex database operations. - -### Model Creation - -- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. - -### APIs & Eloquent Resources - -- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. - -## Controllers & Validation - -- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. -- Check sibling Form Requests to see if the application uses array or string based validation rules. - -## Authentication & Authorization - -- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). - -## URL Generation - -- When generating links to other pages, prefer named routes and the `route()` function. - -## Queues - -- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. - -## Configuration - -- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. - -## Testing - -- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. -- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. -- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. - -## Vite Error - -- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. - -=== laravel/v12 rules === - -# Laravel 12 - -- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples. -- Since Laravel 11, Laravel has a new streamlined file structure which this project uses. - -## Laravel 12 Structure - -- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`. -- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`. -- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files. -- `bootstrap/providers.php` contains application specific service providers. -- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration. -- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration. - -## Database - -- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. -- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. - -### Models - -- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. - -=== pint/core rules === - -# Laravel Pint Code Formatter - -- You must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. -- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. - -=== phpunit/core rules === - -# PHPUnit - -- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit {name}` to create a new test. -- If you see a test using "Pest", convert it to PHPUnit. -- Every time a test has been updated, run that singular test. -- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing. -- Tests should cover all happy paths, failure paths, and edge cases. -- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files; these are core to the application. - -## Running Tests - -- Run the minimal number of tests, using an appropriate filter, before finalizing. -- To run all tests: `php artisan test --compact`. -- To run all tests in a file: `php artisan test --compact tests/Feature/ExampleTest.php`. -- To filter on a particular test name: `php artisan test --compact --filter=testName` (recommended after making a change to a related file). - -=== tailwindcss/core rules === - -# Tailwind CSS - -- Always use existing Tailwind conventions; check project patterns before adding new ones. -- IMPORTANT: Always use `search-docs` tool for version-specific Tailwind CSS documentation and updated code examples. Never rely on training data. -- IMPORTANT: Activate `tailwindcss-development` every time you're working with a Tailwind CSS or styling-related task. - - diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 8a06999..7756775 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -38,7 +38,6 @@ class AuthController extends Controller Auth::guard('web')->logout(); $request->session()->invalidate(); - $request->session()->regenerateToken(); return response()->json(['message' => 'Logged out.']); } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 8677cd5..e7f7c94 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -2,7 +2,9 @@ namespace App\Http\Controllers; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; + abstract class Controller { - // + use AuthorizesRequests; } diff --git a/app/Http/Controllers/MovieController.php b/app/Http/Controllers/MovieController.php index 16b1278..ad94aa9 100644 --- a/app/Http/Controllers/MovieController.php +++ b/app/Http/Controllers/MovieController.php @@ -56,11 +56,10 @@ class MovieController extends Controller * @throws MovieNotFoundException * @throws MovieDatabaseException */ - public function search(Request $request) + public function search(MovieDbInterface $movieDb, Request $request, string $query) { - $searchTerm = $request->input('term'); - $movie = $this->movieDb->search($searchTerm); + $movies = $movieDb->search($query, $request->input('options', [])); - return response()->json(['results' => $movie]); + return response()->json(['results' => $movies]); } } diff --git a/app/Http/Controllers/MovieListController.php b/app/Http/Controllers/MovieListController.php index 2ff4864..f10c83f 100644 --- a/app/Http/Controllers/MovieListController.php +++ b/app/Http/Controllers/MovieListController.php @@ -3,6 +3,9 @@ namespace App\Http\Controllers; use App\Http\Requests\CreateMovieListRequest; +use App\Http\Requests\UpdateMovieListRequest; +use App\Interfaces\MovieDbInterface; +use App\Models\Movie; use App\Models\MovieList; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\Request; @@ -23,6 +26,8 @@ class MovieListController extends Controller */ public function store(CreateMovieListRequest $request) { + $this->authorize('create', MovieList::class); + $validated = $request->validated(); $movieList = MovieList::create([ ...$validated, @@ -38,6 +43,7 @@ class MovieListController extends Controller */ public function show(MovieList $movieList) { + $this->authorize('view', $movieList); try { return $movieList->load('movies'); } catch (ModelNotFoundException $e) { @@ -48,7 +54,7 @@ class MovieListController extends Controller /** * Update the specified resource in storage. */ - public function update(Request $request, MovieList $movieList) + public function update(UpdateMovieListRequest $request, MovieList $movieList) { $validated = $request->validated(); $movieList->update($validated); @@ -61,6 +67,29 @@ class MovieListController extends Controller */ public function destroy(MovieList $movieList) { + $this->authorize('delete', $movieList); $movieList->delete(); } + + public function addMovie(MovieDbInterface $movieDb, Request $request, MovieList $movieList) + { + $this->authorize('update', $movieList); + $movieResult = $movieDb->find($request->input('movie')['imdbId'], ['type' => 'imdb']); + $movie = Movie::where('imdb_id', $movieResult->imdbId)->first(); + + $movieList->movies()->attach($movie); + $movieList->load('movies'); + + return response()->json($movieList); + } + + public function removeMovie(MovieDbInterface $movieDb, Request $request, MovieList $movieList, Movie $movie) + { + $this->authorize('update', $movieList); + + $movieList->movies()->detach($movie); + $movieList->load('movies'); + + return response()->json($movieList); + } } diff --git a/app/Http/Requests/UpdateMovieListRequest.php b/app/Http/Requests/UpdateMovieListRequest.php new file mode 100644 index 0000000..f9b8bef --- /dev/null +++ b/app/Http/Requests/UpdateMovieListRequest.php @@ -0,0 +1,31 @@ +user()->can('update', $this->route('movieList')); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string|max:255', + 'is_public' => 'boolean', + 'movies' => 'array', + 'slug' => 'string', + ]; + } +} diff --git a/app/Interfaces/MovieDbInterface.php b/app/Interfaces/MovieDbInterface.php index c5f4287..b73bd7a 100644 --- a/app/Interfaces/MovieDbInterface.php +++ b/app/Interfaces/MovieDbInterface.php @@ -19,7 +19,7 @@ interface MovieDbInterface * @throws MovieNotFoundException If no movies match the query * @throws MovieDatabaseException If the external movie database is unreachable or returns an error */ - public function search(string $query): Collection; + public function search(string $query, array $options): Collection; /** * Find a specific movie by title or external ID. diff --git a/app/Models/Movie.php b/app/Models/Movie.php index 086509c..caddde3 100644 --- a/app/Models/Movie.php +++ b/app/Models/Movie.php @@ -22,4 +22,11 @@ class Movie extends Model 'poster', 'added_by', ]; + + protected function casts(): array + { + return [ + 'critic_scores' => 'array', + ]; + } } diff --git a/app/Policies/MovieListPolicy.php b/app/Policies/MovieListPolicy.php new file mode 100644 index 0000000..444899a --- /dev/null +++ b/app/Policies/MovieListPolicy.php @@ -0,0 +1,49 @@ +owner === $user->getKey() || $movieList->isPublic) { + return true; + } + + return false; + } + + public function update(User $user, MovieList $movieList): bool + { + if ($movieList->owner === $user->getKey()) { + return true; + } + + return false; + } + + public function delete(User $user, MovieList $movieList): bool + { + if ($movieList->owner === $user->getKey()) { + return true; + } + + return false; + } +} diff --git a/app/Services/OmdbMovieService.php b/app/Services/OmdbMovieService.php index 77565f3..72e381a 100644 --- a/app/Services/OmdbMovieService.php +++ b/app/Services/OmdbMovieService.php @@ -82,7 +82,7 @@ class OmdbMovieService implements MovieDbInterface 'plot' => $movieDetails->plot, 'genre' => $movieDetails->genre, 'mpaa_rating' => $movieDetails->mpaaRating, - 'critic_scores' => $movieDetails->criticScores, + 'critic_scores' => json_encode($movieDetails->criticScores), 'poster' => $movieDetails->poster, 'added_by' => auth()->id(), ]); @@ -169,7 +169,7 @@ class OmdbMovieService implements MovieDbInterface 'plot' => $movieDetails->plot, 'genre' => $movieDetails->genre, 'mpaa_rating' => $movieDetails->mpaaRating, - 'critic_scores' => $movieDetails->criticScores, + 'critic_scores' => json_encode($movieDetails->criticScores), 'poster' => $movieDetails->poster, 'added_by' => auth()->id(), ]); @@ -182,9 +182,9 @@ class OmdbMovieService implements MovieDbInterface * * @throws ConnectionException If connection to OMDB fails */ - public function search(string $query): Collection + public function search(string $query, array $options = []): Collection { - return $this->searchByTitle($query); + return $this->searchByTitle($query, $options); } /** @@ -197,9 +197,13 @@ class OmdbMovieService implements MovieDbInterface * @throws MovieDatabaseException If OMDB API returns an error * @throws MovieNotFoundException If no movies are found */ - private function searchByTitle(string $title): Collection + private function searchByTitle(string $title, array $options): Collection { - $searchResults = $this->makeOmdbRequest(['apikey' => $this->apiKey, 's' => $title, 'type' => 'movie']); + $searchResults = $this->makeOmdbRequest([ + 'apikey' => $this->apiKey, 's' => $title, + 'type' => 'movie', + ...$options, + ]); return collect($searchResults['Search'] ?? []) ->map(fn ($movie) => new MovieSearchResult( diff --git a/routes/api.php b/routes/api.php index 6867f07..be8b0d9 100644 --- a/routes/api.php +++ b/routes/api.php @@ -16,11 +16,14 @@ Route::middleware('auth:sanctum')->group(function () { Route::get('/user', fn (Request $request) => $request->user()); // Movies - Route::get('/movies/search', [MovieController::class, 'search'])->name('movies.search'); + Route::get('/movies/search/{query}', [MovieController::class, 'search'])->name('movies.search'); // Movie Lists Route::get('/movielists', [MovieListController::class, 'index'])->name('movielists.index'); + Route::put('/movielists/', [MovieListController::class, 'index'])->name('movielists.index'); Route::get('/movielists/{movieList}', [MovieListController::class, 'show'])->name('movielists.show'); Route::post('/movielists', [MovieListController::class, 'store'])->name('movielists.store'); + Route::post('/movielists/{movieList}/movies', [MovieListController::class, 'addMovie'])->name('movielists.addMovie'); + Route::delete('/movielists/{movieList}/movies/{movie}', [MovieListController::class, 'removeMovie'])->name('movielists.removeMovie'); Route::delete('/movielists/{movieList}', [MovieListController::class, 'destroy'])->name('movielists.destroy'); });