diff --git a/.ai/guidelines/actions.blade.php b/.ai/guidelines/actions.blade.php
index 406c6d2..1abeb1b 100644
--- a/.ai/guidelines/actions.blade.php
+++ b/.ai/guidelines/actions.blade.php
@@ -1,6 +1,6 @@
# Laravel Action Pattern
-- This application uses the Action pattern and prefers for much logic to live in reusable and composable Action classes.
+- This application uses the Action pattern and business logic should be encapsulated in reusable and composable Action classes.
- Actions live in `app/Actions/`, they are named based on what they do, with no suffix.
- Actions will be called from many different places: jobs, commands, HTTP requests, API requests, MCP requests, and more.
- Create dedicated Action classes for business logic with a single `handle()` method.
diff --git a/.cursor/rules/all.mdc b/.cursor/rules/laravel-boost.mdc
similarity index 75%
rename from .cursor/rules/all.mdc
rename to .cursor/rules/laravel-boost.mdc
index 9645830..0b4d826 100644
--- a/.cursor/rules/all.mdc
+++ b/.cursor/rules/laravel-boost.mdc
@@ -1,53 +1,107 @@
---
-description:
-globs:
alwaysApply: true
---
+
+=== 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 enhance the user's satisfaction building Laravel applications.
-## Conventions
+## 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.12
+- inertiajs/inertia-laravel (INERTIA) - v2
+- laravel/framework (LARAVEL) - v12
+- laravel/mcp (MCP) - v0
+- laravel/passport (PASSPORT) - v13
+- laravel/prompts (PROMPTS) - v0
+- laravel/socialite (SOCIALITE) - v5
+- laravel/wayfinder (WAYFINDER) - v0
+- laravel/pint (PINT) - v1
+- laravel/sail (SAIL) - v1
+- pestphp/pest (PEST) - v4
+- phpunit/phpunit (PHPUNIT) - v12
+- @inertiajs/react (INERTIA) - v2
+- react (REACT) - v19
+- tailwindcss (TAILWINDCSS) - v4
+- @laravel/vite-plugin-wayfinder (WAYFINDER) - v0
+- eslint (ESLINT) - v9
+- prettier (PRETTIER) - v3
+
+## 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, 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 it works. 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.
## Replies
-
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
## Documentation Files
-
- You must only create documentation files if explicitly requested by the user.
+
+=== 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.
+
+## 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 any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
+- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
+- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches.
+- Search the documentation before making code changes to ensure we are taking the correct approach.
+- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
+- 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
+- You can and should pass multiple queries at once. The most relevant results will be returned first.
+
+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 if it has one line.
-- Always use `declare(strict_types=1);` at the top of PHP files.
### Constructors
-
- Use PHP 8 constructor property promotion in `__construct()`.
- - public function \_\_construct(public GitHub $github) { }
+ - public function __construct(public GitHub $github) { }
- Do not allow empty `__construct()` methods with zero parameters.
### Type Declarations
-
- Always use explicit return type declarations for methods and functions.
- Use appropriate PHP type hints for method parameters.
@@ -59,24 +113,23 @@ protected function isAccessible(User $user, ?string $path = null): bool
## Comments
-
- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on.
## PHPDoc Blocks
-
- Add useful array shape type definitions for arrays when appropriate.
## Enums
+- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
-- Keys in an Enum should be uppercase. For example: `FAVORITE_PERSON`, `BEST_LAKE`, `MONTHLY`.
=== herd rules ===
## Laravel Herd
-- The application is served by Laravel Herd and will be available at: https?://locket.test. If available, use the `get-absolute-url` tool to generate URLs for the user to ensure valid URLs.
+- The application is served by Laravel Herd and will be available at: https?://[kebab-case-project-dir].test. Use the `get-absolute-url` tool to generate URLs for the user to ensure valid URLs.
- You must not run any commands to make the site available via HTTP(s). It is _always_ available through Laravel Herd.
+
=== inertia-laravel/core rules ===
## Inertia Core
@@ -94,6 +147,7 @@ Route::get('/users', function () {
});
+
=== inertia-laravel/v2 rules ===
## Inertia v2
@@ -101,7 +155,6 @@ Route::get('/users', function () {
- Make use of all Inertia features from v1 & v2. Check the documentation before making any changes to ensure we are taking the correct approach.
### Inertia v2 New Features
-
- Polling
- Prefetching
- Deferred props
@@ -109,15 +162,14 @@ Route::get('/users', function () {
- Lazy loading data on scroll
### Deferred Props & Empty States
-
- When using deferred props on the frontend, you should add a nice empty state with pulsing / animated skeleton.
### Inertia Form General Guidance
-
- The recommended way to build forms when using Inertia is with the `
+
+
+=== .ai/actions rules ===
# Laravel Action Pattern
@@ -458,5 +495,4 @@ public function \_\_construct(protected FavoriteApi $fav) { }
}
-
-- Write Browser tests where appropriate.
+
\ No newline at end of file
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 9f2ccc0..8be1719 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -23,6 +23,20 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
+ tools: composer:v2
+
+ - name: Cache Composer dependencies
+ uses: actions/cache@v4
+ with:
+ path: vendor
+ key: composer-${{ hashFiles('composer.lock') }}
+ restore-keys: composer-
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: 'npm'
- name: Install Dependencies
run: |
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 5db46ce..1d753c2 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -31,6 +31,13 @@ jobs:
node-version: '22'
cache: 'npm'
+ - name: Cache Composer dependencies
+ uses: actions/cache@v4
+ with:
+ path: vendor
+ key: composer-${{ hashFiles('composer.lock') }}
+ restore-keys: composer-
+
- name: Install Node Dependencies
run: npm ci
@@ -47,4 +54,4 @@ jobs:
run: php artisan key:generate
- name: Tests
- run: ./vendor/bin/pest
+ run: ./vendor/bin/pest --parallel
diff --git a/.mcp.json b/.mcp.json
new file mode 100644
index 0000000..8c6715a
--- /dev/null
+++ b/.mcp.json
@@ -0,0 +1,11 @@
+{
+ "mcpServers": {
+ "laravel-boost": {
+ "command": "php",
+ "args": [
+ "artisan",
+ "boost:mcp"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index 9084b24..8319a7f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,47 +1,104 @@
+
+=== 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 enhance the user's satisfaction building Laravel applications.
-## Conventions
+## 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.12
+- inertiajs/inertia-laravel (INERTIA) - v2
+- laravel/framework (LARAVEL) - v12
+- laravel/mcp (MCP) - v0
+- laravel/passport (PASSPORT) - v13
+- laravel/prompts (PROMPTS) - v0
+- laravel/socialite (SOCIALITE) - v5
+- laravel/wayfinder (WAYFINDER) - v0
+- laravel/pint (PINT) - v1
+- laravel/sail (SAIL) - v1
+- pestphp/pest (PEST) - v4
+- phpunit/phpunit (PHPUNIT) - v12
+- @inertiajs/react (INERTIA) - v2
+- react (REACT) - v19
+- tailwindcss (TAILWINDCSS) - v4
+- @laravel/vite-plugin-wayfinder (WAYFINDER) - v0
+- eslint (ESLINT) - v9
+- prettier (PRETTIER) - v3
+
+## 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, 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 it works. 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.
## Replies
-
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
## Documentation Files
-
- You must only create documentation files if explicitly requested by the user.
+
+=== 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.
+
+## 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 any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
+- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
+- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches.
+- Search the documentation before making code changes to ensure we are taking the correct approach.
+- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
+- 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
+- You can and should pass multiple queries at once. The most relevant results will be returned first.
+
+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 if it has one line.
-- Always use `declare(strict_types=1);` at the top of PHP files.
### Constructors
-
- Use PHP 8 constructor property promotion in `__construct()`.
- - public function \_\_construct(public GitHub $github) { }
+ - public function __construct(public GitHub $github) { }
- Do not allow empty `__construct()` methods with zero parameters.
### Type Declarations
-
- Always use explicit return type declarations for methods and functions.
- Use appropriate PHP type hints for method parameters.
@@ -53,24 +110,23 @@ protected function isAccessible(User $user, ?string $path = null): bool
## Comments
-
- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on.
## PHPDoc Blocks
-
- Add useful array shape type definitions for arrays when appropriate.
## Enums
+- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
-- Keys in an Enum should be uppercase. For example: `FAVORITE_PERSON`, `BEST_LAKE`, `MONTHLY`.
=== herd rules ===
## Laravel Herd
-- The application is served by Laravel Herd and will be available at: https?://locket.test. If available, use the `get-absolute-url` tool to generate URLs for the user to ensure valid URLs.
+- The application is served by Laravel Herd and will be available at: https?://[kebab-case-project-dir].test. Use the `get-absolute-url` tool to generate URLs for the user to ensure valid URLs.
- You must not run any commands to make the site available via HTTP(s). It is _always_ available through Laravel Herd.
+
=== inertia-laravel/core rules ===
## Inertia Core
@@ -88,6 +144,7 @@ Route::get('/users', function () {
});
+
=== inertia-laravel/v2 rules ===
## Inertia v2
@@ -95,7 +152,6 @@ Route::get('/users', function () {
- Make use of all Inertia features from v1 & v2. Check the documentation before making any changes to ensure we are taking the correct approach.
### Inertia v2 New Features
-
- Polling
- Prefetching
- Deferred props
@@ -103,15 +159,14 @@ Route::get('/users', function () {
- Lazy loading data on scroll
### Deferred Props & Empty States
-
- When using deferred props on the frontend, you should add a nice empty state with pulsing / animated skeleton.
### Inertia Form General Guidance
-
- The recommended way to build forms when using Inertia is with the `
` component - a useful example is below. Use `search-docs` with a query of `form component` for guidance.
- Forms can also be built using the `useForm` helper for more programmatic control, or to follow existing conventions. Use `search-docs` with a query of `useForm helper` for guidance.
- `resetOnError`, `resetOnSuccess`, and `setDefaultsOnSuccess` are available on the `
` component. Use `search-docs` with a query of 'form component resetting' for guidance.
+
=== laravel/core rules ===
## Do Things the Laravel Way
@@ -121,7 +176,6 @@ Route::get('/users', function () {
- 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.
@@ -129,53 +183,44 @@ Route::get('/users', function () {
- 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.
### Queues
-
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
### Authentication & Authorization
-
-- Use Laravel's built-in authentication and authorization features (gates, policies, etc.).
+- 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.
### 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] ` 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
-- Use the `search-docs` tool to get version specific documentation when available.
+- Use the `search-docs` tool to get version specific documentation.
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
### Laravel 12 Structure
-
- No middleware files in `app/Http/Middleware/`.
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
- `bootstrap/providers.php` contains application specific service providers.
@@ -183,14 +228,13 @@ Route::get('/users', function () {
- **Commands auto-register** - files 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 11 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
@@ -198,29 +242,27 @@ Route::get('/users', function () {
- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues.
+
=== pest/core rules ===
## Pest
### Testing
-
- If you need to verify a feature is working, write or update a Unit / Feature test.
### Pest Tests
-
- All tests must be written using Pest. Use `php artisan make:test --pest `.
- 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.
- Tests should test all of the happy paths, failure paths, and weird paths.
- Tests live in the `tests/Feature` and `tests/Unit` directories.
- Pest tests look and behave like this:
-
- it('is true', function () {
- expect(true)->toBeTrue();
- });
-
+
+it('is true', function () {
+ expect(true)->toBeTrue();
+});
+
### Running Tests
-
- Run the minimal number of tests using an appropriate filter before finalizing code edits.
- To run all tests: `php artisan test`.
- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`.
@@ -228,25 +270,21 @@ Route::get('/users', function () {
- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing.
### Pest Assertions
-
- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.:
-
- it('returns all', function () {
- $response = $this->postJson('/api/docs', []);
-
- $response->assertSuccessful();
+
+it('returns all', function () {
+ $response = $this->postJson('/api/docs', []);
- });
-
+ $response->assertSuccessful();
+});
+
### Mocking
-
- Mocking can be very helpful when appropriate.
- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do.
- You can also create partial mocks using the same import or self method.
### Datasets
-
- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules.
@@ -258,6 +296,7 @@ it('has emails', function (string $email) {
]);
+
=== pest/v4 rules ===
## Pest 4
@@ -268,7 +307,6 @@ it('has emails', function (string $email) {
- Use the `search-docs` tool for detailed guidance on utilizing these features.
### Browser Testing
-
- You can use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories within Pest v4 browser tests, as well as `RefreshDatabase` (when needed) to ensure a clean state for each test.
- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.) when appropriate to complete the test.
- If requested, test on multiple browsers (Chrome, Firefox, Safari).
@@ -294,7 +332,6 @@ it('may reset the password', function () {
->assertSee('We have emailed your password reset link!')
Notification::assertSent(ResetPassword::class);
-
});
@@ -304,6 +341,7 @@ $pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavascriptErrors()->assertNoConsoleLogs();
+
=== inertia-react/core rules ===
## Inertia + React
@@ -313,12 +351,12 @@ $pages->assertNoJavascriptErrors()->assertNoConsoleLogs();
import { Link } from '@inertiajs/react'
-
Home
-=== inertia-react/v2 rules ===
+
+=== inertia-react/v2/forms rules ===
## Inertia + React Forms
@@ -327,20 +365,19 @@ import { Link } from '@inertiajs/react'
import { Form } from '@inertiajs/react'
export default () => (
-
-
-
)
+
=== tailwindcss/core rules ===
## Tailwind Core
@@ -364,22 +401,24 @@ defaults
- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own.
- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..)
- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically
+- You can use the `search-docs` tool to get exact examples from the official documentation when needed.
### Spacing
-
- When listing items, use gap utilities for spacing, don't use margins.
-
-
-
Superior
-
Michigan
-
Erie
-
-
-### Dark Mode
+
+
+
Superior
+
Michigan
+
Erie
+
+
+
+### Dark Mode
- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`.
+
=== tailwindcss/v4 rules ===
## Tailwind 4
@@ -388,41 +427,42 @@ defaults
- `corePlugins` is not supported in Tailwind v4.
- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3:
-
+ - @tailwind base;
+ - @tailwind components;
+ - @tailwind utilities;
+ + @import "tailwindcss";
+
-* @import "tailwindcss";
-
### Replaced Utilities
-
- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement.
- Opacity values are still numeric.
-| Deprecated | Replacement |
+| Deprecated | Replacement |
|------------+--------------|
-| bg-opacity-_ | bg-black/_ |
-| text-opacity-_ | text-black/_ |
-| border-opacity-_ | border-black/_ |
-| divide-opacity-_ | divide-black/_ |
-| ring-opacity-_ | ring-black/_ |
-| placeholder-opacity-_ | placeholder-black/_ |
-| flex-shrink-_ | shrink-_ |
-| flex-grow-_ | grow-_ |
+| bg-opacity-* | bg-black/* |
+| text-opacity-* | text-black/* |
+| border-opacity-* | border-black/* |
+| divide-opacity-* | divide-black/* |
+| ring-opacity-* | ring-black/* |
+| placeholder-opacity-* | placeholder-black/* |
+| flex-shrink-* | shrink-* |
+| flex-grow-* | grow-* |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |
+
=== tests rules ===
## Test Enforcement
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter.
-
+
+
+=== .ai/actions rules ===
# Laravel Action Pattern
@@ -452,5 +492,4 @@ public function \_\_construct(protected FavoriteApi $fav) { }
}
-
-- Write Browser tests where appropriate.
+
\ No newline at end of file
diff --git a/app/Actions/AddLink.php b/app/Actions/AddLink.php
index c1ac5f6..aeebdeb 100644
--- a/app/Actions/AddLink.php
+++ b/app/Actions/AddLink.php
@@ -17,6 +17,8 @@ final class AddLink
{
/**
* Add a link (or find existing) and create user bookmark.
+ *
+ * @return array{link: array{id: int, url: string, title: string, description: string, category: string}, user_link: array{id: int, category: string, status: string, created_at: string}, already_bookmarked: bool}
*/
public function handle(string $url, User $user, ?string $categoryHint = null): array
{
diff --git a/app/Actions/AddLinkNote.php b/app/Actions/AddLinkNote.php
index c1dc93f..c714aef 100644
--- a/app/Actions/AddLinkNote.php
+++ b/app/Actions/AddLinkNote.php
@@ -14,6 +14,8 @@ final class AddLinkNote
{
/**
* Add a personal note to a user's bookmarked link.
+ *
+ * @return array{note: array{id: int, note: string, created_at: string}, link_id: int}
*/
public function handle(int $linkId, string $note, User $user): array
{
diff --git a/app/Actions/CreateStatusWithLink.php b/app/Actions/CreateStatusWithLink.php
index 23539dc..bbaeb1b 100644
--- a/app/Actions/CreateStatusWithLink.php
+++ b/app/Actions/CreateStatusWithLink.php
@@ -18,6 +18,8 @@ public function __construct(
/**
* Add a link and create a status update mentioning it.
+ *
+ * @return array{link: array{id: int, url: string, title: string, description: string, category: string}, user_link: array{id: int, category: string, status: string, created_at: string}, status: array{id: int, status: string, created_at: string}, already_bookmarked: bool, note?: array{id: int, note: string, created_at: string}}
*/
public function handle(string $url, ?string $thoughts, User $user, ?string $categoryHint = null): array
{
diff --git a/app/Actions/GetAllRecentStatuses.php b/app/Actions/GetAllRecentStatuses.php
index ae25c8e..99e9722 100644
--- a/app/Actions/GetAllRecentStatuses.php
+++ b/app/Actions/GetAllRecentStatuses.php
@@ -14,6 +14,8 @@ final class GetAllRecentStatuses
* Get recent statuses as minimal payload.
*
* When a user is provided, only that user's statuses are returned.
+ *
+ * @return array
*/
public function handle(int $limit = 10, ?User $user = null): array
{
diff --git a/app/Actions/GetRecentLinks.php b/app/Actions/GetRecentLinks.php
index 5a79392..e504195 100644
--- a/app/Actions/GetRecentLinks.php
+++ b/app/Actions/GetRecentLinks.php
@@ -10,6 +10,8 @@ final class GetRecentLinks
{
/**
* Get the most recently added links.
+ *
+ * @return array
*/
public function handle(int $limit = 10): array
{
@@ -24,6 +26,7 @@ public function handle(int $limit = 10): array
'title' => $link->title,
'description' => $link->description,
'category' => $link->category->value,
+ /** @phpstan-ignore-next-line nullsafe.neverNull */
'submitted_by' => $link->submittedBy?->name ?? 'Anonymous',
'created_at' => $link->created_at->diffForHumans(),
];
diff --git a/app/Actions/GetTrendingLinksToday.php b/app/Actions/GetTrendingLinksToday.php
index 12afb70..31cbcbd 100644
--- a/app/Actions/GetTrendingLinksToday.php
+++ b/app/Actions/GetTrendingLinksToday.php
@@ -12,6 +12,8 @@ final class GetTrendingLinksToday
{
/**
* Get the top trending links for today based on user bookmarks.
+ *
+ * @return array
*/
public function handle(int $limit = 10): array
{
diff --git a/app/Actions/GetUserLastAddedLink.php b/app/Actions/GetUserLastAddedLink.php
index 7ceff9d..35e6a8a 100644
--- a/app/Actions/GetUserLastAddedLink.php
+++ b/app/Actions/GetUserLastAddedLink.php
@@ -11,9 +11,12 @@ final class GetUserLastAddedLink
{
/**
* Get the user's most recently added link with its notes.
+ *
+ * @return array{user_link: array{id: int, category: string, status: string, created_at: string}, link: array{id: int, url: string, title: string, description: string, category: string}, notes: array}|null
*/
public function handle(User $user): ?array
{
+ /** @var UserLink|null $userLink */
$userLink = UserLink::with(['link', 'notes' => function ($query) use ($user) {
$query->forUser($user->id)->recent();
}])
@@ -39,13 +42,15 @@ public function handle(User $user): ?array
'description' => $userLink->link->description,
'category' => $userLink->link->category->value,
],
- 'notes' => $userLink->notes->map(function ($note) {
- return [
- 'id' => $note->id,
- 'note' => $note->note,
- 'created_at' => $note->created_at->diffForHumans(),
- ];
- })->toArray(),
+ 'notes' => $userLink->notes->map(
+ function (\App\Models\LinkNote $note, int $key) {
+ return [
+ 'id' => $note->id,
+ 'note' => $note->note,
+ 'created_at' => $note->created_at->diffForHumans(),
+ ];
+ }
+ )->toArray(),
];
}
}
diff --git a/app/Console/Commands/FetchLinkTitle.php b/app/Console/Commands/FetchLinkTitle.php
index 2d0dc5b..e1c65e3 100644
--- a/app/Console/Commands/FetchLinkTitle.php
+++ b/app/Console/Commands/FetchLinkTitle.php
@@ -36,7 +36,7 @@ public function handle(): int
if ($debug) {
// Fetch the HTML directly to show what we're getting
- $this->info("Fetching HTML with debug mode...");
+ $this->info('Fetching HTML with debug mode...');
try {
$response = \Illuminate\Support\Facades\Http::timeout(10)
@@ -45,28 +45,28 @@ public function handle(): int
$html = $response->body();
- $this->info("Response Status: " . $response->status());
- $this->info("Content Type: " . $response->header('Content-Type'));
- $this->info("HTML Length: " . strlen($html) . " bytes");
+ $this->info('Response Status: '.$response->status());
+ $this->info('Content Type: '.$response->header('Content-Type'));
+ $this->info('HTML Length: '.strlen($html).' bytes');
// Look for title tag
if (preg_match('/]*>(.*?)<\/title>/is', $html, $matches)) {
- $this->info("Raw title tag content: " . $matches[1]);
- $this->info("Cleaned title: " . html_entity_decode(trim($matches[1])));
+ $this->info('Raw title tag content: '.$matches[1]);
+ $this->info('Cleaned title: '.html_entity_decode(trim($matches[1])));
} else {
- $this->warn("No title tag found!");
- $this->info("First 1000 chars of HTML:");
+ $this->warn('No title tag found!');
+ $this->info('First 1000 chars of HTML:');
$this->line(substr($html, 0, 1000));
}
$this->info("\n---Now running the actual job---\n");
} catch (\Exception $e) {
- $this->error("Debug fetch failed: " . $e->getMessage());
+ $this->error('Debug fetch failed: '.$e->getMessage());
}
}
// Create a temporary Link model (not saved to database)
- $link = new Link();
+ $link = new Link;
$link->id = 0;
$link->url = $url;
$link->title = null;
@@ -76,8 +76,9 @@ public function handle(): int
// Override the update method to just display the result
$link->fillable(['title']);
- $originalUpdate = \Closure::bind(function($attributes) {
+ $originalUpdate = \Closure::bind(function ($attributes) {
$this->fill($attributes);
+
return true;
}, $link, Link::class);
@@ -89,10 +90,11 @@ public function handle(): int
if ($link->title) {
$this->info("✓ Successfully fetched title: {$link->title}");
} else {
- $this->warn("No title was fetched for this URL");
+ $this->warn('No title was fetched for this URL');
}
} catch (\Exception $e) {
$this->error("Error: {$e->getMessage()}");
+
return Command::FAILURE;
}
diff --git a/app/Http/Controllers/Auth/SocialiteController.php b/app/Http/Controllers/Auth/SocialiteController.php
index fe2f073..2a58c98 100644
--- a/app/Http/Controllers/Auth/SocialiteController.php
+++ b/app/Http/Controllers/Auth/SocialiteController.php
@@ -8,10 +8,11 @@
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
+use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
class SocialiteController
{
- public function redirectToGitHub(): RedirectResponse
+ public function redirectToGitHub(): SymfonyRedirectResponse
{
return Socialite::driver('github')->redirect();
}
diff --git a/app/Http/Controllers/Settings/ProfileController.php b/app/Http/Controllers/Settings/ProfileController.php
index e9844e7..bffa300 100644
--- a/app/Http/Controllers/Settings/ProfileController.php
+++ b/app/Http/Controllers/Settings/ProfileController.php
@@ -24,11 +24,10 @@ public function edit(Request $request): Response
->where('revoked', false)
->orderBy('created_at', 'desc')
->get()
- ->map(function ($token) {
+ ->map(function (\Laravel\Passport\Token $token) {
return [
'id' => $token->id,
'name' => $token->name,
- 'last_used_at' => $token->last_used_at?->toDateTimeString(),
'created_at' => $token->created_at->toDateTimeString(),
];
});
@@ -83,25 +82,11 @@ public function destroy(Request $request): RedirectResponse
/**
* Create a new API token.
*/
- public function createToken(CreateApiTokenRequest $request): RedirectResponse|JsonResponse
+ public function createToken(CreateApiTokenRequest $request): JsonResponse|RedirectResponse
{
$tokenResult = $request->user()->createToken($request->validated()['name']);
- // For API requests (not Inertia), return JSON
- if ($request->wantsJson() && ! $request->header('X-Inertia')) {
- return response()->json([
- 'token' => $tokenResult->accessToken,
- 'accessToken' => [
- 'id' => $tokenResult->token->id,
- 'name' => $tokenResult->token->name,
- 'last_used_at' => $tokenResult->token->last_used_at,
- 'created_at' => $tokenResult->token->created_at,
- ],
- ]);
- }
-
- // For Inertia and regular web requests, store token in session then redirect
- // Using session instead of flash because flash doesn't always work with Inertia redirects
+ // For web/Inertia requests, store token in session then redirect
session(['created_token' => $tokenResult->accessToken]);
return back();
@@ -110,24 +95,16 @@ public function createToken(CreateApiTokenRequest $request): RedirectResponse|Js
/**
* Revoke an API token.
*/
- public function revokeToken(Request $request, string $tokenId): RedirectResponse|JsonResponse
+ public function revokeToken(Request $request, string $tokenId): JsonResponse|RedirectResponse
{
$token = $request->user()->tokens()->where('id', $tokenId)->first();
if (! $token) {
- if ($request->wantsJson()) {
- return response()->json(['error' => 'Token not found'], 404);
- }
-
return back()->withErrors(['token' => 'Token not found or could not be revoked.']);
}
$token->revoke();
- if ($request->wantsJson()) {
- return response()->json(['message' => 'Token revoked successfully']);
- }
-
return back()->with('message', 'Token revoked successfully.');
}
}
diff --git a/app/Http/Resources/UserStatusResource.php b/app/Http/Resources/UserStatusResource.php
index 0ae3b7d..e2b446e 100644
--- a/app/Http/Resources/UserStatusResource.php
+++ b/app/Http/Resources/UserStatusResource.php
@@ -20,14 +20,14 @@ class UserStatusResource extends JsonResource
*/
public function toArray(Request $request): array
{
- $email = strtolower(trim($this->user?->email ?? ''));
+ $email = strtolower(trim($this->user->email));
$hash = md5($email);
$gravatar = "https://www.gravatar.com/avatar/{$hash}?s=128&d=404";
$fallback = 'https://avatars.laravel.cloud/'.urlencode($email).'?vibe=stealth';
// Use GitHub avatar if available, otherwise fall back to Gravatar
- $avatar = $this->user?->avatar ?? $gravatar;
- $displayName = $this->user?->github_username ?? $this->user?->name ?? 'Unknown';
+ $avatar = $this->user->avatar ?? $gravatar;
+ $displayName = $this->user->github_username ?? $this->user->name ?? 'Unknown';
return [
'id' => $this->id,
@@ -35,16 +35,16 @@ public function toArray(Request $request): array
'created_at' => $this->created_at?->toAtomString(),
'user' => [
'name' => $displayName,
- 'github_username' => $this->user?->github_username,
+ 'github_username' => $this->user->github_username ?? null,
'avatar' => $avatar,
'avatar_fallback' => $fallback,
],
- 'link' => $this->link ? [
+ 'link' => [
'id' => $this->link->id,
'url' => $this->link->url,
'title' => $this->link->title,
'description' => $this->link->description,
- ] : null,
+ ],
];
}
}
diff --git a/app/Mcp/Resources/LastAddedLink.php b/app/Mcp/Resources/LastAddedLink.php
index beb620e..875ea14 100644
--- a/app/Mcp/Resources/LastAddedLink.php
+++ b/app/Mcp/Resources/LastAddedLink.php
@@ -14,11 +14,12 @@ class LastAddedLink extends Resource
public function handle(Request $request, GetUserLastAddedLink $getUserLastAddedLink): string
{
- if (! $request->user()) {
+ $user = $request->user();
+ if (! $user instanceof \App\Models\User) {
return "❌ **Authentication Required**\n\nYou must be authenticated to view your last added link.";
}
- $result = $getUserLastAddedLink->handle($request->user());
+ $result = $getUserLastAddedLink->handle($user);
if (! $result) {
return "⚠️ **No Links Found**\n\nYou haven't added any links to your Locket yet. Try adding your first link!";
diff --git a/app/Mcp/Tools/AddLink.php b/app/Mcp/Tools/AddLink.php
index ea3983c..6289f4a 100644
--- a/app/Mcp/Tools/AddLink.php
+++ b/app/Mcp/Tools/AddLink.php
@@ -33,7 +33,7 @@ public function handle(Request $request): Response
$user = $request->user();
- if (! $user) {
+ if (! $user instanceof \App\Models\User) {
return Response::error('Authentication required to add links');
}
@@ -78,12 +78,10 @@ public function schema(JsonSchema $schema): array
->description('The URL to add to your reading list')
->required(),
'thoughts' => $schema->string()
- ->description('Optional thoughts or notes about this link (will be saved as a private note)')
- ->required(false),
+ ->description('Optional thoughts or notes about this link (will be saved as a private note)'),
'category_hint' => $schema->string()
->enum(['read', 'reference', 'watch', 'tools'])
- ->description('Optional category hint: read (articles/blogs), reference (docs/specs), watch (videos), tools (libraries/services)')
- ->required(false),
+ ->description('Optional category hint: read (articles/blogs), reference (docs/specs), watch (videos), tools (libraries/services)'),
];
}
}
diff --git a/app/Models/Link.php b/app/Models/Link.php
index 3543add..90c9c82 100644
--- a/app/Models/Link.php
+++ b/app/Models/Link.php
@@ -13,6 +13,9 @@
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
+/**
+ * @property-read User|null $submittedBy
+ */
class Link extends Model
{
/** @use HasFactory<\Database\Factories\LinkFactory> */
diff --git a/app/Models/UserLink.php b/app/Models/UserLink.php
index a11c22c..49c4d7d 100644
--- a/app/Models/UserLink.php
+++ b/app/Models/UserLink.php
@@ -13,6 +13,11 @@
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
+/**
+ * @property-read Link $link
+ * @property-read User $user
+ * @property-read \Illuminate\Database\Eloquent\Collection $notes
+ */
class UserLink extends Model
{
/** @use HasFactory<\Database\Factories\UserLinkFactory> */
diff --git a/app/Models/UserStatus.php b/app/Models/UserStatus.php
index 86d805d..6a90e34 100644
--- a/app/Models/UserStatus.php
+++ b/app/Models/UserStatus.php
@@ -9,6 +9,10 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
+/**
+ * @property-read User $user
+ * @property-read Link $link
+ */
class UserStatus extends Model
{
/** @use HasFactory<\Database\Factories\UserStatusFactory> */
@@ -41,6 +45,8 @@ public function link(): BelongsTo
/**
* Convert this status to its frontend representation.
+ *
+ * @return array
*/
public function toFrontendFormat(): array
{
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 44a4b36..b1c5ba0 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -35,6 +35,7 @@ public function boot(): void
/**
* @param array{client: \Laravel\Passport\Client, user: \App\Models\User, scopes: array, request: \Illuminate\Http\Request, authToken: string} $parameters
*/
+ /** @phpstan-ignore-next-line */
Passport::authorizationView(function ($parameters) {
return view('auth.authorize', $parameters);
});
diff --git a/composer.json b/composer.json
index 1a2e7a5..899f2c9 100644
--- a/composer.json
+++ b/composer.json
@@ -19,7 +19,8 @@
},
"require-dev": {
"fakerphp/faker": "^1.23",
- "laravel/boost": "1.2.0",
+ "larastan/larastan": "^3.0",
+ "laravel/boost": "^1.2",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.18",
"laravel/sail": "^1.41",
diff --git a/composer.lock b/composer.lock
index 93cd7cd..1d915fc 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "9baf6cf3a8ca4b486b20bdf6073e2f0f",
+ "content-hash": "40d5b643219c05317b33e3c863e0c410",
"packages": [
{
"name": "brick/math",
@@ -9107,6 +9107,47 @@
},
"time": "2025-04-30T06:54:44+00:00"
},
+ {
+ "name": "iamcal/sql-parser",
+ "version": "v0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/iamcal/SQLParser.git",
+ "reference": "947083e2dca211a6f12fb1beb67a01e387de9b62"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/947083e2dca211a6f12fb1beb67a01e387de9b62",
+ "reference": "947083e2dca211a6f12fb1beb67a01e387de9b62",
+ "shasum": ""
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^1.0",
+ "phpunit/phpunit": "^5|^6|^7|^8|^9"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "iamcal\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Cal Henderson",
+ "email": "cal@iamcal.com"
+ }
+ ],
+ "description": "MySQL schema parser",
+ "support": {
+ "issues": "https://github.com/iamcal/SQLParser/issues",
+ "source": "https://github.com/iamcal/SQLParser/tree/v0.6"
+ },
+ "time": "2025-03-17T16:59:46+00:00"
+ },
{
"name": "jean85/pretty-package-versions",
"version": "2.1.1",
@@ -9225,6 +9266,95 @@
},
"time": "2023-02-03T21:26:53+00:00"
},
+ {
+ "name": "larastan/larastan",
+ "version": "v3.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/larastan/larastan.git",
+ "reference": "2e653fd19585a825e283b42f38378b21ae481cc7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/larastan/larastan/zipball/2e653fd19585a825e283b42f38378b21ae481cc7",
+ "reference": "2e653fd19585a825e283b42f38378b21ae481cc7",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "iamcal/sql-parser": "^0.6.0",
+ "illuminate/console": "^11.44.2 || ^12.4.1",
+ "illuminate/container": "^11.44.2 || ^12.4.1",
+ "illuminate/contracts": "^11.44.2 || ^12.4.1",
+ "illuminate/database": "^11.44.2 || ^12.4.1",
+ "illuminate/http": "^11.44.2 || ^12.4.1",
+ "illuminate/pipeline": "^11.44.2 || ^12.4.1",
+ "illuminate/support": "^11.44.2 || ^12.4.1",
+ "php": "^8.2",
+ "phpstan/phpstan": "^2.1.23"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^13",
+ "laravel/framework": "^11.44.2 || ^12.7.2",
+ "mockery/mockery": "^1.6.12",
+ "nikic/php-parser": "^5.4",
+ "orchestra/canvas": "^v9.2.2 || ^10.0.1",
+ "orchestra/testbench-core": "^9.12.0 || ^10.1",
+ "phpstan/phpstan-deprecation-rules": "^2.0.1",
+ "phpunit/phpunit": "^10.5.35 || ^11.5.15"
+ },
+ "suggest": {
+ "orchestra/testbench": "Using Larastan for analysing a package needs Testbench"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Larastan\\Larastan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Can Vural",
+ "email": "can9119@gmail.com"
+ }
+ ],
+ "description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel",
+ "keywords": [
+ "PHPStan",
+ "code analyse",
+ "code analysis",
+ "larastan",
+ "laravel",
+ "package",
+ "php",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/larastan/larastan/issues",
+ "source": "https://github.com/larastan/larastan/tree/v3.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/canvural",
+ "type": "github"
+ }
+ ],
+ "time": "2025-09-10T19:42:11+00:00"
+ },
{
"name": "laravel/boost",
"version": "v1.2.0",
@@ -10771,6 +10901,64 @@
},
"time": "2025-08-30T15:50:23+00:00"
},
+ {
+ "name": "phpstan/phpstan",
+ "version": "2.1.27",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan.git",
+ "reference": "25da374959afa391992792691093550b3098ef1e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/25da374959afa391992792691093550b3098ef1e",
+ "reference": "25da374959afa391992792691093550b3098ef1e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "docs": "https://phpstan.org/user-guide/getting-started",
+ "forum": "https://github.com/phpstan/phpstan/discussions",
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "security": "https://github.com/phpstan/phpstan/security/policy",
+ "source": "https://github.com/phpstan/phpstan-src"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ }
+ ],
+ "time": "2025-09-17T09:55:13+00:00"
+ },
{
"name": "phpunit/php-code-coverage",
"version": "12.3.5",
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..b8d5923
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,9 @@
+includes:
+ - vendor/larastan/larastan/extension.neon
+ - vendor/nesbot/carbon/extension.neon
+
+parameters:
+ paths:
+ - app/
+
+ level: 5
diff --git a/resources/js/components/delete-user.tsx b/resources/js/components/delete-user.tsx
index fa03993..d13d0b6 100644
--- a/resources/js/components/delete-user.tsx
+++ b/resources/js/components/delete-user.tsx
@@ -1,10 +1,7 @@
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
import HeadingSmall from '@/components/heading-small';
-import InputError from '@/components/input-error';
import { Button } from '@/components/ui/button';
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
import { Form } from '@inertiajs/react';
import { useRef } from 'react';
diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx
index f56cc15..5d8c8ca 100644
--- a/resources/js/pages/welcome.tsx
+++ b/resources/js/pages/welcome.tsx
@@ -406,7 +406,7 @@ function TrendingLinksSection({
))}
- Locket is your social link sharing read-later app for developers
+ Locket is your social link sharing read-later app for developers
);
}
diff --git a/tests/Feature/Settings/PassportTokenManagementTest.php b/tests/Feature/Settings/PassportTokenManagementTest.php
index 0857732..655c2e4 100644
--- a/tests/Feature/Settings/PassportTokenManagementTest.php
+++ b/tests/Feature/Settings/PassportTokenManagementTest.php
@@ -14,23 +14,13 @@
$user = User::factory()->create();
$this->actingAs($user);
- $response = $this->postJson(route('profile.tokens.create'), [
+ $response = $this->post(route('profile.tokens.create'), [
'name' => 'Test Token',
]);
- $response->assertSuccessful();
- $response->assertJsonStructure([
- 'token',
- 'accessToken' => [
- 'id',
- 'name',
- 'last_used_at',
- 'created_at',
- ],
- ]);
-
- expect($response->json('accessToken.name'))->toBe('Test Token');
- expect($user->tokens()->count())->toBe(1);
+ $response->assertRedirect();
+ expect($user->fresh()->tokens()->count())->toBe(1);
+ expect($user->fresh()->tokens()->first()->name)->toBe('Test Token');
});
it('can list personal access tokens', function () {
@@ -60,10 +50,9 @@
$this->actingAs($user);
- $response = $this->deleteJson(route('profile.tokens.revoke', $token->token->id));
+ $response = $this->delete(route('profile.tokens.revoke', $token->token->id));
- $response->assertSuccessful();
- $response->assertJson(['message' => 'Token revoked successfully']);
+ $response->assertRedirect();
// Check token is revoked
$user->refresh();
@@ -78,22 +67,22 @@
$this->actingAs($user1);
- $response = $this->deleteJson(route('profile.tokens.revoke', $token->token->id));
+ $response = $this->delete(route('profile.tokens.revoke', $token->token->id));
- $response->assertNotFound();
- $response->assertJson(['error' => 'Token not found']);
+ $response->assertRedirect();
+ $response->assertSessionHasErrors('token');
});
it('validates token name when creating', function () {
$user = User::factory()->create();
$this->actingAs($user);
- $response = $this->postJson(route('profile.tokens.create'), [
+ $response = $this->post(route('profile.tokens.create'), [
'name' => '',
]);
- $response->assertUnprocessable();
- $response->assertJsonValidationErrors('name');
+ $response->assertRedirect();
+ $response->assertSessionHasErrors('name');
});
it('can use personal access token to access API', function () {
diff --git a/tests/Feature/UserStatusResourceTest.php b/tests/Feature/UserStatusResourceTest.php
index 474c664..57c6037 100644
--- a/tests/Feature/UserStatusResourceTest.php
+++ b/tests/Feature/UserStatusResourceTest.php
@@ -38,17 +38,26 @@
->and($result['user']['avatar_fallback'])->toContain('avatars.laravel.cloud');
});
-it('handles missing user gracefully', function () {
- $status = UserStatus::factory()->make([
+it('handles user without github username', function () {
+ $user = User::factory()->create([
+ 'name' => 'Jane Doe',
+ 'email' => 'jane@example.com',
+ 'github_username' => null,
+ 'avatar' => null,
+ ]);
+
+ $status = UserStatus::factory()->create([
+ 'user_id' => $user->id,
'status' => 'Test status',
- 'user_id' => null,
]);
- $status->user = null;
+
+ $status->load('user');
$resource = new UserStatusResource($status);
$result = $resource->toArray(new Request);
- expect($result['user']['name'])->toBe('Unknown')
+ expect($result['user']['name'])->toBe('Jane Doe')
+ ->and($result['user']['github_username'])->toBeNull()
->and($result['user']['avatar'])->toContain('gravatar.com/avatar')
->and($result['user']['avatar_fallback'])->toContain('avatars.laravel.cloud');
});