first commit
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/vendor
|
||||
/node_modules
|
||||
/.phpunit.cache
|
||||
/.fleet
|
||||
/.idea
|
||||
/.vscode
|
||||
.DS_Store
|
||||
272
README.MD
Normal file
272
README.MD
Normal file
@@ -0,0 +1,272 @@
|
||||
# Page Bookmarks
|
||||
|
||||
A simple bookmark management system for Laravel Filament applications. This package provides an intuitive way for users to save, organize, and access bookmarks directly within your Admin panel.
|
||||
|
||||
## Features
|
||||
|
||||
- 📚 **Smart Bookmark Creation**: Automatically captures the current page URL and title
|
||||
- 📁 **Folder Organization**: Organize bookmarks into custom folders
|
||||
- 🔍 **Real-time Search**: Search through your bookmarks with instant filtering
|
||||
- ⌨️ **Keyboard Shortcuts**: Quick bookmark creation shortcut `Cmd+Shift+B` on Mac or `Ctrl+Shift+B` on Windows/Linux
|
||||
- 👤 **User-specific**: Each user has their own private bookmarks
|
||||
- 🎯 **Customizable**: Configurable icons, render hooks, table names
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Add Bookmark
|
||||

|
||||
|
||||
### View Bookmarks
|
||||

|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP 8.3+
|
||||
- Laravel 10+
|
||||
- Filament 3.2+
|
||||
- Livewire 3+
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Install the package via Composer:**
|
||||
|
||||
```bash
|
||||
composer require jaysontemporas/page-bookmarks
|
||||
```
|
||||
|
||||
2. **Publish and run the installation command:**
|
||||
|
||||
```bash
|
||||
php artisan page-bookmarks:install
|
||||
```
|
||||
|
||||
This command will:
|
||||
- Publish the configuration file
|
||||
- Publish and run the database migrations
|
||||
- Publish the assets
|
||||
|
||||
## Configuration
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
The package configuration file is located at `config/page-bookmarks.php`. Here are the main configuration options:
|
||||
|
||||
```php
|
||||
return [
|
||||
// Table names (customizable to avoid conflicts)
|
||||
'tables' => [
|
||||
'bookmarks' => 'bookmarks',
|
||||
'bookmark_folders' => 'bookmark_folders',
|
||||
],
|
||||
|
||||
// User model for bookmark associations
|
||||
'models' => [
|
||||
'user' => \App\Models\User::class,
|
||||
],
|
||||
|
||||
// Icons used throughout the interface
|
||||
'icons' => [
|
||||
'add_bookmark' => 'heroicon-o-folder-plus',
|
||||
'view_bookmarks' => 'heroicon-o-bookmark',
|
||||
'bookmark_item' => 'heroicon-o-bookmark',
|
||||
'folder' => 'heroicon-o-folder',
|
||||
'search' => 'heroicon-o-magnifying-glass',
|
||||
'delete' => 'heroicon-o-trash',
|
||||
'chevron_down' => 'heroicon-o-chevron-down',
|
||||
'empty_state' => 'heroicon-o-bookmark',
|
||||
],
|
||||
|
||||
// Render hook positions in Filament
|
||||
'render_hooks' => [
|
||||
'add_bookmark' => \Filament\View\PanelsRenderHook::GLOBAL_SEARCH_AFTER,
|
||||
'view_bookmarks' => \Filament\View\PanelsRenderHook::GLOBAL_SEARCH_AFTER,
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
This package utilizes Filament's theming system, so you'll need to set up a custom theme to properly style all components.
|
||||
|
||||
> [!NOTE] Before proceeding, ensure you have configured a custom theme if you're using Filament Panels. Check the [Filament documentation on themes](https://filamentphp.com/docs/3.x/panels/themes) for detailed instructions. This step is required for both the Panels Package and standalone Forms package.
|
||||
|
||||
To properly compile all the package styles, update your Tailwind configuration by adding the package's view paths
|
||||
|
||||
### Customizing Render Hooks
|
||||
|
||||
You can customize where the bookmark components appear in your Filament panel by modifying the `render_hooks` configuration:
|
||||
|
||||
```php
|
||||
'render_hooks' => [
|
||||
'add_bookmark' => \Filament\View\PanelsRenderHook::GLOBAL_SEARCH_AFTER,
|
||||
'view_bookmarks' => \Filament\View\PanelsRenderHook::GLOBAL_SEARCH_AFTER,
|
||||
],
|
||||
```
|
||||
|
||||
For a complete list of available render hook options, please refer to the [official Filament documentation](https://filamentphp.com/docs/3.x/support/render-hooks). The documentation includes all available `PanelsRenderHook` constants and their specific use cases.
|
||||
|
||||
## Usage
|
||||
|
||||
### Adding the HasBookmarks Trait
|
||||
|
||||
To enable bookmark functionality for your User model, add the `HasBookmarks` trait:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use JaysonTemporas\PageBookmarks\Traits\HasBookmarks;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasBookmarks;
|
||||
|
||||
// ... rest of your User model
|
||||
}
|
||||
```
|
||||
|
||||
### Using Bookmarks in Your Application
|
||||
|
||||
Once the trait is added, you can use the bookmark functionality in your application:
|
||||
|
||||
```php
|
||||
// Get user's bookmarks
|
||||
$user = auth()->user();
|
||||
$bookmarks = $user->bookmarks;
|
||||
|
||||
// Get bookmark folders
|
||||
$folders = $user->bookmarkFolders;
|
||||
|
||||
// Create a new bookmark
|
||||
$bookmark = $user->createBookmark([
|
||||
'name' => 'My Bookmark',
|
||||
'url' => 'https://example.com',
|
||||
'bookmark_folder_id' => $folder->id, // optional
|
||||
]);
|
||||
|
||||
// Create a new folder
|
||||
$folder = $user->createBookmarkFolder([
|
||||
'name' => 'Work Bookmarks',
|
||||
]);
|
||||
|
||||
// Get bookmarks in a specific folder
|
||||
$workBookmarks = $user->bookmarksInFolder($folder);
|
||||
|
||||
// Check if user has bookmarks
|
||||
if ($user->hasBookmarks()) {
|
||||
// User has bookmarks
|
||||
}
|
||||
|
||||
// Get bookmark counts
|
||||
$bookmarkCount = $user->getBookmarksCount();
|
||||
$folderCount = $user->getBookmarkFoldersCount();
|
||||
```
|
||||
|
||||
### Available Methods
|
||||
|
||||
The `HasBookmarks` trait provides the following methods:
|
||||
|
||||
- `bookmarks()` - Get all bookmarks for the user
|
||||
- `bookmarkFolders()` - Get all bookmark folders for the user
|
||||
- `rootBookmarks()` - Get bookmarks not in any folder
|
||||
- `createBookmarkFolder(array $attributes)` - Create a new bookmark folder
|
||||
- `createBookmark(array $attributes)` - Create a new bookmark
|
||||
- `bookmarksInFolder(BookmarkFolder $folder)` - Get bookmarks in a specific folder
|
||||
- `bookmarksInFolderById(int $folderId)` - Get bookmarks in a folder by ID
|
||||
- `hasBookmarks()` - Check if user has any bookmarks
|
||||
- `hasBookmarkFolders()` - Check if user has any bookmark folders
|
||||
- `getBookmarksCount()` - Get total bookmark count
|
||||
- `getBookmarkFoldersCount()` - Get total folder count
|
||||
|
||||
## User Interface
|
||||
|
||||
### Adding Bookmarks
|
||||
|
||||
1. **Click the bookmark icon** in the Filament panel header
|
||||
2. **Use keyboard shortcut** (default: `Cmd+Shift+B` on Mac or `Ctrl+Shift+B` on Windows/Linux)
|
||||
3. The current page URL and title will be automatically captured
|
||||
4. Enter a custom name (optional - defaults to page title)
|
||||
5. Select or create a folder (optional)
|
||||
6. Click "Save"
|
||||
|
||||
### Viewing Bookmarks
|
||||
|
||||
1. **Click the bookmark viewer icon** in the Filament panel header
|
||||
2. Browse your bookmarks organized by folders
|
||||
3. Use the search bar to filter bookmarks
|
||||
4. Click on any bookmark to navigate to the saved URL
|
||||
5. Hover over bookmarks to reveal the delete button
|
||||
|
||||
### Managing Bookmarks
|
||||
|
||||
- **Search**: Type in the search bar to filter bookmarks by name
|
||||
- **Folders**: Click on folder headers to expand/collapse
|
||||
- **Delete**: Hover over a bookmark and click the trash icon to delete
|
||||
- **Navigation**: Click on any bookmark to navigate to the saved URL
|
||||
|
||||
## Database Schema
|
||||
|
||||
The package creates two tables:
|
||||
|
||||
### `bookmarks` table
|
||||
- `id` - Primary key
|
||||
- `user_id` - Foreign key to users table
|
||||
- `name` - Bookmark name/title
|
||||
- `bookmark_folder_id` - Foreign key to bookmark_folders table (nullable)
|
||||
- `url` - The saved URL
|
||||
- `created_at` - Creation timestamp
|
||||
- `updated_at` - Last update timestamp
|
||||
|
||||
### `bookmark_folders` table
|
||||
- `id` - Primary key
|
||||
- `user_id` - Foreign key to users table
|
||||
- `name` - Folder name
|
||||
- `created_at` - Creation timestamp
|
||||
- `updated_at` - Last update timestamp
|
||||
|
||||
## Customization
|
||||
|
||||
### Custom Icons
|
||||
|
||||
You can customize the icons used throughout the interface by modifying the `icons` configuration:
|
||||
|
||||
```php
|
||||
'icons' => [
|
||||
'add_bookmark' => 'heroicon-o-bookmark-square',
|
||||
'view_bookmarks' => 'heroicon-o-bookmark',
|
||||
// ... other icons
|
||||
],
|
||||
```
|
||||
|
||||
### Custom Table Names
|
||||
|
||||
If you need to avoid table name conflicts, you can customize the table names:
|
||||
|
||||
```php
|
||||
'tables' => [
|
||||
'bookmarks' => 'my_custom_bookmarks',
|
||||
'bookmark_folders' => 'my_custom_bookmark_folders',
|
||||
],
|
||||
```
|
||||
|
||||
### Custom User Model
|
||||
|
||||
If you have a custom User model, update the configuration:
|
||||
|
||||
```php
|
||||
'models' => [
|
||||
'user' => \App\Models\CustomUser::class,
|
||||
],
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
This package is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter any issues or have questions, please open an issue on the GitHub repository.
|
||||
BIN
assets/add_bookmark.png
Normal file
BIN
assets/add_bookmark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 241 KiB |
BIN
assets/view_bookmark.png
Normal file
BIN
assets/view_bookmark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 249 KiB |
33
composer.json
Normal file
33
composer.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "jaysontemporas/page-bookmarks",
|
||||
"version": "1.0.0",
|
||||
"type": "library",
|
||||
"require-dev": {
|
||||
"php": "^8.3",
|
||||
"laravel/pint": "^1.17"
|
||||
},
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Jaysontemporas\\PageBookmarks\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Jaysontemporas\\PageBookmarks\\PageBookmarksServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jayson Temporas"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"filament/filament": "^3.2",
|
||||
"spatie/laravel-package-tools": "^1.18"
|
||||
}
|
||||
}
|
||||
7340
composer.lock
generated
Normal file
7340
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
87
config/page-bookmarks.php
Normal file
87
config/page-bookmarks.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pages Bookmarks Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Table Names
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you can customize the table names used by the page bookmarks package.
|
||||
| You can change these if you need to avoid conflicts with existing tables
|
||||
| or prefer different naming conventions.
|
||||
|
|
||||
*/
|
||||
'tables' => [
|
||||
'bookmarks' => 'bookmarks',
|
||||
'bookmark_folders' => 'bookmark_folders',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Model
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the model that will be used to associate bookmarks with users.
|
||||
| You can change this to your own User model if you have a custom one.
|
||||
|
|
||||
*/
|
||||
'models' => [
|
||||
'user' => \App\Models\User::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Modal Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure how the bookmark modals should be displayed.
|
||||
| Options: 'modal' or 'slideOver'
|
||||
| Default: 'slideOver' for both
|
||||
|
|
||||
*/
|
||||
'modal' => [
|
||||
'add_bookmark' => 'slideOver', // 'modal' or 'modal'
|
||||
'view_bookmarks' => 'slideOver', // 'modal' or 'slideOver'
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Icons Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure the icons used throughout the page bookmarks interface.
|
||||
| You can use any Heroicon or custom icon names that are available
|
||||
| in your Filament installation.
|
||||
|
|
||||
*/
|
||||
'icons' => [
|
||||
'add_bookmark' => 'heroicon-o-folder-plus',
|
||||
'view_bookmarks' => 'heroicon-o-bookmark',
|
||||
'bookmark_item' => 'heroicon-o-bookmark',
|
||||
'folder' => 'heroicon-o-folder',
|
||||
'search' => 'heroicon-o-magnifying-glass',
|
||||
'delete' => 'heroicon-o-trash',
|
||||
'chevron_down' => 'heroicon-o-chevron-down',
|
||||
'empty_state' => 'heroicon-o-bookmark',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Render Hooks Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure where the bookmark manager and viewer components should be
|
||||
| rendered in the Filament panel. You can use any of the available
|
||||
| PanelsRenderHook constants from Filament\View\PanelsRenderHook.
|
||||
*/
|
||||
'render_hooks' => [
|
||||
'add_bookmark' => \Filament\View\PanelsRenderHook::GLOBAL_SEARCH_AFTER,
|
||||
'view_bookmarks' => \Filament\View\PanelsRenderHook::GLOBAL_SEARCH_AFTER,
|
||||
],
|
||||
];
|
||||
36
database/migrations/create_bookmarks_table.php
Normal file
36
database/migrations/create_bookmarks_table.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$bookmarkFoldersTable = config('page-bookmarks.tables.bookmark_folders', 'bookmark_folders');
|
||||
$bookmarksTable = config('page-bookmarks.tables.bookmarks', 'bookmarks');
|
||||
|
||||
Schema::create($bookmarkFoldersTable, function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create($bookmarksTable, function (Blueprint $table) use ($bookmarkFoldersTable): void {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->foreignId('bookmark_folder_id')->nullable()->constrained($bookmarkFoldersTable)->nullOnDelete();
|
||||
$table->text('url');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists(config('page-bookmarks.tables.bookmarks', 'bookmarks'));
|
||||
Schema::dropIfExists(config('page-bookmarks.tables.bookmark_folders', 'bookmark_folders'));
|
||||
}
|
||||
};
|
||||
42
resources/views/livewire/bookmark-manager.blade.php
Normal file
42
resources/views/livewire/bookmark-manager.blade.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<div class="flex justify-end">
|
||||
<!-- Bookmark Icon Button -->
|
||||
<x-filament::icon-button
|
||||
icon="{{ $this->getIcons()['add_bookmark'] }}"
|
||||
class="text-gray-500 transition-colors hover:text-primary-500"
|
||||
x-on:click="$dispatch('open-modal', { id: 'bookmark-form-modal' }); $nextTick(() => {
|
||||
// Try to get the title from h1 tag
|
||||
const h1 = document.querySelector('h1');
|
||||
const pageTitle = h1 ? h1.textContent.trim() : document.title;
|
||||
|
||||
// Dispatch event to Livewire to set the title
|
||||
$wire.setBookmarkName(pageTitle);
|
||||
})"
|
||||
x-on:keydown.meta.shift.b.prevent.document="$dispatch('open-modal', { id: 'bookmark-form-modal' }); $nextTick(() => {
|
||||
// Try to get the title from h1 tag
|
||||
const h1 = document.querySelector('h1');
|
||||
const pageTitle = h1 ? h1.textContent.trim() : document.title;
|
||||
|
||||
// Dispatch event to Livewire to set the title
|
||||
$wire.setBookmarkName(pageTitle);
|
||||
})"
|
||||
/>
|
||||
|
||||
<x-filament::modal
|
||||
id="bookmark-form-modal"
|
||||
width="md"
|
||||
:slide-over="config('page-bookmarks.modal.add_bookmark') === 'slideOver' ? true : false"
|
||||
heading="Add Bookmark"
|
||||
>
|
||||
<form wire:submit.prevent="save">
|
||||
{{ $this->form }}
|
||||
|
||||
<div class="flex justify-end mt-6 gap-x-2">
|
||||
<x-filament::button type="submit">
|
||||
Save
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</form>
|
||||
</x-filament::modal>
|
||||
|
||||
<x-filament-actions::modals />
|
||||
</div>
|
||||
164
resources/views/livewire/bookmark-viewer.blade.php
Normal file
164
resources/views/livewire/bookmark-viewer.blade.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<div class="flex">
|
||||
<x-filament::icon-button
|
||||
icon="{{ $this->getIcons()['view_bookmarks'] }}"
|
||||
color="gray"
|
||||
x-on:click="$dispatch('open-modal', { id: 'bookmark-items-modal' })"
|
||||
/>
|
||||
|
||||
<x-filament::modal
|
||||
id="bookmark-items-modal"
|
||||
width="md"
|
||||
heading="My Bookmarks"
|
||||
:slide-over="config('page-bookmarks.modal.view_bookmarks') === 'slideOver' ? true : false"
|
||||
x-on:open-modal.window="if ($event.detail.id === 'bookmark-items-modal') $wire.$refresh()"
|
||||
x-on:refreshBookmarks.window="$wire.$refresh()"
|
||||
>
|
||||
<div class="px-2 mb-4">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 flex items-center pointer-events-none start-0 ps-3">
|
||||
<x-filament::icon
|
||||
icon="{{ $this->getIcons()['search'] }}"
|
||||
class="w-4 h-4 text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="search"
|
||||
class="w-full p-2 text-sm text-gray-900 border border-gray-300 rounded-lg ps-10 bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
|
||||
placeholder="Search bookmarks"
|
||||
x-data
|
||||
x-on:input.debounce.300ms="
|
||||
const searchTerm = $event.target.value.toLowerCase();
|
||||
|
||||
// Get all bookmark items
|
||||
const bookmarkItems = document.querySelectorAll('[data-bookmark-item]');
|
||||
|
||||
// Track visible items per folder
|
||||
const folderVisibleCount = {};
|
||||
|
||||
// First check all bookmark items
|
||||
bookmarkItems.forEach(item => {
|
||||
const name = item.getAttribute('data-bookmark-name').toLowerCase();
|
||||
const folder = item.getAttribute('data-bookmark-folder');
|
||||
|
||||
const isVisible = name.includes(searchTerm);
|
||||
item.style.display = isVisible ? 'flex' : 'none';
|
||||
|
||||
// Count visible items
|
||||
if (isVisible) {
|
||||
folderVisibleCount[folder] = (folderVisibleCount[folder] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Then show/hide folder containers
|
||||
document.querySelectorAll('[data-folder-container]').forEach(folder => {
|
||||
const folderName = folder.getAttribute('data-folder-name');
|
||||
const counter = folder.querySelector('[data-folder-counter]');
|
||||
|
||||
const visibleCount = folderVisibleCount[folderName] || 0;
|
||||
|
||||
if (counter) {
|
||||
counter.textContent = visibleCount;
|
||||
}
|
||||
|
||||
folder.style.display = visibleCount > 0 ? 'block' : 'none';
|
||||
});
|
||||
"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="px-2 -mx-2"
|
||||
wire:key="bookmarks-list"
|
||||
>
|
||||
@forelse($this->bookmarksByFolder as $folder => $bookmarks)
|
||||
<div
|
||||
wire:key="folder-{{ $loop->index }}"
|
||||
x-data="{ open: true }"
|
||||
class="mb-3"
|
||||
data-folder-container
|
||||
data-folder-name="{{ $folder }}"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between px-2 py-2 text-sm font-medium rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
@click="open = !open"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<x-filament::icon
|
||||
icon="{{ $this->getIcons()['folder'] }}"
|
||||
class="w-5 h-5 mr-2 text-gray-400 shrink-0 dark:text-gray-500"
|
||||
/>
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
{{ ucfirst($folder) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
class="px-1.5 py-0.5 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 mr-1"
|
||||
data-folder-counter
|
||||
>{{ count($bookmarks) }}</span>
|
||||
<x-filament::icon
|
||||
icon="{{ $this->getIcons()['chevron_down'] }}"
|
||||
class="w-4 h-4 text-gray-400 transition-transform duration-300"
|
||||
x-bind:class="{ 'rotate-0': open, '-rotate-90': !open }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-1 space-y-1"
|
||||
x-show="open"
|
||||
x-collapse
|
||||
>
|
||||
<div class="pl-4 ml-3 border-l border-gray-200 dark:border-gray-700">
|
||||
@foreach ($bookmarks as $bookmark)
|
||||
<div
|
||||
class="relative flex items-center justify-between px-2 py-1.5 mb-1 text-gray-600 group dark:text-gray-400 hover:bg-primary-500/10 hover:text-primary-600 dark:hover:text-primary-500 rounded-md transition duration-150"
|
||||
wire:key="bookmark-{{ $bookmark->id }}"
|
||||
data-bookmark-item
|
||||
data-bookmark-name="{{ $bookmark->name }}"
|
||||
data-bookmark-folder="{{ $folder }}"
|
||||
>
|
||||
<a
|
||||
href="{{ $bookmark->url }}"
|
||||
class="flex items-center w-full truncate"
|
||||
title="{{ $bookmark->name }}"
|
||||
>
|
||||
<x-filament::icon
|
||||
icon="{{ $this->getIcons()['bookmark_item'] }}"
|
||||
class="w-4 h-4 mr-2 text-gray-400 shrink-0 dark:text-gray-500"
|
||||
/>
|
||||
<span class="text-sm truncate">{{ $bookmark->name }}</span>
|
||||
</a>
|
||||
|
||||
<div class="transition duration-300 opacity-0 group-hover:opacity-100">
|
||||
<x-filament::icon-button
|
||||
icon="{{ $this->getIcons()['delete'] }}"
|
||||
color="danger"
|
||||
size="xs"
|
||||
wire:click.stop="deleteBookmark({{ $bookmark->id }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="deleteBookmark({{ $bookmark->id }})"
|
||||
class="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="py-6 text-center">
|
||||
<x-filament::icon
|
||||
icon="{{ $this->getIcons()['empty_state'] }}"
|
||||
class="w-12 h-12 mx-auto text-gray-400"
|
||||
/>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">No bookmarks found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Click the bookmark plus icon to save your first bookmark.
|
||||
</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</x-filament::modal>
|
||||
</div>
|
||||
271
src/Livewire/BookmarkManager.php
Normal file
271
src/Livewire/BookmarkManager.php
Normal file
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace JaysonTemporas\PageBookmarks\Livewire;
|
||||
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Collection;
|
||||
use JaysonTemporas\PageBookmarks\Models\Bookmark;
|
||||
use JaysonTemporas\PageBookmarks\Models\BookmarkFolder;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
*/
|
||||
class BookmarkManager extends Component implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
// Get current URL from the request
|
||||
$currentUrl = request()->url();
|
||||
|
||||
// Initialize data with the current URL
|
||||
$this->data = [
|
||||
'url' => $currentUrl,
|
||||
'display_url' => $currentUrl,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available bookmark folders for the current user
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
#[Computed]
|
||||
public function availableBookmarkFolders(): array
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if ($user === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var array<int, string> $folders */
|
||||
$folders = BookmarkFolder::query()->where('user_id', $user->id)
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
|
||||
return $folders;
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Hidden::make('url'),
|
||||
|
||||
Select::make('bookmark_folder_id')
|
||||
->label('Folder')
|
||||
->options(BookmarkFolder::query()->where('user_id', auth()->id())->pluck('name', 'id'))
|
||||
->createOptionForm([
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
])
|
||||
->createOptionUsing(function (array $data, Set $set) {
|
||||
$user = auth()->user();
|
||||
|
||||
if ($user === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user->bookmarkFolders()->create($data)->getKey();
|
||||
})
|
||||
->nullable(),
|
||||
|
||||
TextInput::make('display_url')
|
||||
->label('URL')
|
||||
->disabled()
|
||||
->dehydrated(false),
|
||||
])
|
||||
->model(Bookmark::class)
|
||||
->statePath('data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bookmark name from JavaScript
|
||||
*/
|
||||
public function setBookmarkName(string $name): void
|
||||
{
|
||||
$this->data['name'] = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bookmarks organized by folders
|
||||
*
|
||||
* @return Collection<string, Collection<int, Bookmark>>
|
||||
*/
|
||||
#[Computed]
|
||||
public function bookmarksByFolder(): Collection
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if ($user === null) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$bookmarks = Bookmark::query()->where('user_id', $user->id)
|
||||
->with('folder')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
// Group by bookmark folder (or 'Uncategorized' if folder is null)
|
||||
$grouped = $bookmarks->groupBy(function (Bookmark $bookmark) {
|
||||
if ($bookmark->folder) {
|
||||
return $bookmark->folder->name;
|
||||
}
|
||||
|
||||
// Fallback to the old folder field for backward compatibility
|
||||
return $bookmark->folder ?: 'Uncategorized';
|
||||
});
|
||||
|
||||
/** @var Collection<string, Collection<int, Bookmark>> */
|
||||
$result = collect();
|
||||
|
||||
// Convert to the right Collection types for PHPStan
|
||||
foreach ($grouped as $folder => $items) {
|
||||
/** @var Collection<int, Bookmark> */
|
||||
$bookmarkCollection = collect($items);
|
||||
$result->put($folder, $bookmarkCollection);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a bookmark
|
||||
*/
|
||||
public function deleteBookmark(int $id): void
|
||||
{
|
||||
$bookmark = Bookmark::query()->where('user_id', auth()->id())->find($id);
|
||||
|
||||
if ($bookmark) {
|
||||
$bookmark->delete();
|
||||
|
||||
Notification::make()
|
||||
->duration(1200)
|
||||
->title('Bookmark deleted successfully')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh bookmarks from event
|
||||
*/
|
||||
#[On('refreshBookmarks')]
|
||||
public function refreshBookmarks(): void
|
||||
{
|
||||
// This will refresh the computed property
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configured icons
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getIcons(): array
|
||||
{
|
||||
return config('page-bookmarks.icons', [
|
||||
'bookmark_manager' => 'heroicon-o-folder-plus',
|
||||
'bookmark_viewer' => 'heroicon-o-bookmark',
|
||||
'bookmark_item' => 'heroicon-o-bookmark',
|
||||
'folder' => 'heroicon-o-folder',
|
||||
'search' => 'heroicon-o-magnifying-glass',
|
||||
'delete' => 'heroicon-o-trash',
|
||||
'chevron_down' => 'heroicon-o-chevron-down',
|
||||
'empty_state' => 'heroicon-o-bookmark',
|
||||
]);
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('page-bookmarks::livewire.bookmark-manager');
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = $this->form->getState();
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
if ($user === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract and sanitize input values
|
||||
$name = isset($data['name']) && is_string($data['name']) ? $data['name'] : '';
|
||||
$url = isset($data['url']) && is_string($data['url']) ? $data['url'] : request()->url();
|
||||
$bookmarkFolderId = isset($data['bookmark_folder_id']) && is_numeric($data['bookmark_folder_id'])
|
||||
? (int) $data['bookmark_folder_id']
|
||||
: null;
|
||||
|
||||
// Check for existing bookmarks with the same name or URL for this user
|
||||
$existingBookmark = Bookmark::query()->where('user_id', $user->id)
|
||||
->where(function ($query) use ($name, $url): void {
|
||||
$query->where('name', $name)
|
||||
->orWhere('url', $url);
|
||||
})
|
||||
->first();
|
||||
|
||||
if ($existingBookmark) {
|
||||
// Determine if it's a duplicate name, URL, or both
|
||||
$duplicateField = '';
|
||||
if ($existingBookmark->name === $name && $existingBookmark->url === $url) {
|
||||
$duplicateField = 'bookmark with this name and URL';
|
||||
} elseif ($existingBookmark->name === $name) {
|
||||
$duplicateField = 'bookmark with this name';
|
||||
} else {
|
||||
$duplicateField = 'bookmark for this URL';
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('You already have a '.$duplicateField)
|
||||
->warning()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and save the new bookmark
|
||||
$bookmark = new Bookmark;
|
||||
$bookmark->user_id = $user->id;
|
||||
$bookmark->name = $name;
|
||||
$bookmark->url = $url;
|
||||
$bookmark->bookmark_folder_id = $bookmarkFolderId;
|
||||
$bookmark->save();
|
||||
|
||||
$this->form->fill([
|
||||
'url' => request()->url(),
|
||||
]);
|
||||
|
||||
$this->dispatch('close-modal', id: 'bookmark-form-modal');
|
||||
$this->dispatch('refreshBookmarks');
|
||||
|
||||
Notification::make()
|
||||
->title('Bookmark saved successfully')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
113
src/Livewire/BookmarkViewer.php
Normal file
113
src/Livewire/BookmarkViewer.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace JaysonTemporas\PageBookmarks\Livewire;
|
||||
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Collection;
|
||||
use JaysonTemporas\PageBookmarks\Models\Bookmark;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Lazy;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Lazy]
|
||||
class BookmarkViewer extends Component
|
||||
{
|
||||
/**
|
||||
* Get bookmarks organized by folders
|
||||
*
|
||||
* @return Collection<string, Collection<int, Bookmark>>
|
||||
*/
|
||||
#[Computed]
|
||||
public function bookmarksByFolder(): Collection
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if ($user === null) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$bookmarks = Bookmark::query()->where('user_id', $user->id)
|
||||
->with('folder')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
// Group by bookmark folder (or 'Uncategorized' if folder is null)
|
||||
$grouped = $bookmarks->groupBy(function (Bookmark $bookmark) {
|
||||
if ($bookmark->folder) {
|
||||
return $bookmark->folder->name;
|
||||
}
|
||||
|
||||
// Fallback to the old folder field for backward compatibility
|
||||
return $bookmark->folder ?: 'Uncategorized';
|
||||
});
|
||||
|
||||
/** @var Collection<string, Collection<int, Bookmark>> */
|
||||
$result = collect();
|
||||
|
||||
// Convert to the right Collection types for PHPStan
|
||||
foreach ($grouped as $folder => $items) {
|
||||
/** @var Collection<int, Bookmark> */
|
||||
$bookmarkCollection = collect($items);
|
||||
$result->put($folder, $bookmarkCollection);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a bookmark
|
||||
*/
|
||||
public function deleteBookmark(int $id): void
|
||||
{
|
||||
$bookmark = Bookmark::query()->where('user_id', auth()->id())->find($id);
|
||||
|
||||
if ($bookmark) {
|
||||
$bookmark->delete();
|
||||
|
||||
Notification::make()
|
||||
->duration(2000)
|
||||
->title('Bookmark deleted successfully')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$this->dispatch('refreshBookmarks');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh bookmarks from event
|
||||
*/
|
||||
#[On('refreshBookmarks')]
|
||||
public function refreshBookmarks(): void
|
||||
{
|
||||
// This will refresh the computed property
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configured icons
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getIcons(): array
|
||||
{
|
||||
return config('page-bookmarks.icons', [
|
||||
'bookmark_manager' => 'heroicon-o-folder-plus',
|
||||
'bookmark_viewer' => 'heroicon-o-bookmark',
|
||||
'bookmark_item' => 'heroicon-o-bookmark',
|
||||
'folder' => 'heroicon-o-folder',
|
||||
'search' => 'heroicon-o-magnifying-glass',
|
||||
'delete' => 'heroicon-o-trash',
|
||||
'chevron_down' => 'heroicon-o-chevron-down',
|
||||
'empty_state' => 'heroicon-o-bookmark',
|
||||
]);
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('page-bookmarks::livewire.bookmark-viewer');
|
||||
}
|
||||
}
|
||||
26
src/Models/Bookmark.php
Normal file
26
src/Models/Bookmark.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace JaysonTemporas\PageBookmarks\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Bookmark extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'name',
|
||||
'bookmark_folder_id',
|
||||
'url',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(config('page-bookmarks.models.user', config('auth.providers.users.model')));
|
||||
}
|
||||
|
||||
public function folder(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BookmarkFolder::class, 'bookmark_folder_id');
|
||||
}
|
||||
}
|
||||
25
src/Models/BookmarkFolder.php
Normal file
25
src/Models/BookmarkFolder.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace JaysonTemporas\PageBookmarks\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class BookmarkFolder extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'name',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(config('page-bookmarks.models.user', config('auth.providers.users.model')));
|
||||
}
|
||||
|
||||
public function bookmarks(): HasMany
|
||||
{
|
||||
return $this->hasMany(Bookmark::class, 'bookmark_folder_id');
|
||||
}
|
||||
}
|
||||
32
src/PageBookmarksPlugin.php
Normal file
32
src/PageBookmarksPlugin.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace JaysonTemporas\PageBookmarks;
|
||||
|
||||
use Filament\Contracts\Plugin;
|
||||
use Filament\Panel;
|
||||
|
||||
class PageBookmarksPlugin implements Plugin
|
||||
{
|
||||
public function getId(): string
|
||||
{
|
||||
return 'jaysontemporas-page-bookmarks';
|
||||
}
|
||||
|
||||
public function register(Panel $panel): void
|
||||
{
|
||||
$panel
|
||||
->resources([
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
public function boot(Panel $panel): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public static function make(): static
|
||||
{
|
||||
return new static;
|
||||
}
|
||||
}
|
||||
50
src/PageBookmarksServiceProvider.php
Normal file
50
src/PageBookmarksServiceProvider.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace JaysonTemporas\PageBookmarks;
|
||||
|
||||
use Filament\Support\Facades\FilamentView;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use JaysonTemporas\PageBookmarks\Livewire\BookmarkManager;
|
||||
use JaysonTemporas\PageBookmarks\Livewire\BookmarkViewer;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\LaravelPackageTools\Package;
|
||||
use Spatie\LaravelPackageTools\PackageServiceProvider;
|
||||
|
||||
class PageBookmarksServiceProvider extends PackageServiceProvider
|
||||
{
|
||||
public static string $name = 'page-bookmarks';
|
||||
|
||||
public function configurePackage(Package $package): void
|
||||
{
|
||||
$package
|
||||
->name(static::$name)
|
||||
->hasConfigFile()
|
||||
->hasMigration('create_bookmarks_table')
|
||||
->hasViews('page-bookmarks')
|
||||
// Publishing groups
|
||||
->hasInstallCommand(function ($command) {
|
||||
$command
|
||||
->publishConfigFile()
|
||||
->publishMigrations()
|
||||
->publishAssets()
|
||||
->askToRunMigrations();
|
||||
});
|
||||
}
|
||||
|
||||
public function packageBooted(): void
|
||||
{
|
||||
// Register the Livewire component
|
||||
Livewire::component('page-bookmarks::livewire.bookmark-manager', BookmarkManager::class);
|
||||
Livewire::component('page-bookmarks::livewire.bookmark-viewer', BookmarkViewer::class);
|
||||
|
||||
FilamentView::registerRenderHook(
|
||||
config('page-bookmarks.render_hooks.add_bookmark', \Filament\View\PanelsRenderHook::GLOBAL_SEARCH_AFTER),
|
||||
fn (): string => Blade::render("@livewire('page-bookmarks::livewire.bookmark-manager')"),
|
||||
);
|
||||
|
||||
FilamentView::registerRenderHook(
|
||||
config('page-bookmarks.render_hooks.view_bookmarks', \Filament\View\PanelsRenderHook::GLOBAL_SEARCH_AFTER),
|
||||
fn (): string => Blade::render("@livewire('page-bookmarks::livewire.bookmark-viewer')"),
|
||||
);
|
||||
}
|
||||
}
|
||||
98
src/Traits/HasBookmarks.php
Normal file
98
src/Traits/HasBookmarks.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace JaysonTemporas\PageBookmarks\Traits;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use JaysonTemporas\PageBookmarks\Models\Bookmark;
|
||||
use JaysonTemporas\PageBookmarks\Models\BookmarkFolder;
|
||||
|
||||
trait HasBookmarks
|
||||
{
|
||||
/**
|
||||
* Get all bookmarks for the user.
|
||||
*/
|
||||
public function bookmarks(): HasMany
|
||||
{
|
||||
return $this->hasMany(Bookmark::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all bookmark folders for the user.
|
||||
*/
|
||||
public function bookmarkFolders(): HasMany
|
||||
{
|
||||
return $this->hasMany(BookmarkFolder::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bookmarks that are not in any folder (root level bookmarks).
|
||||
*/
|
||||
public function rootBookmarks(): HasMany
|
||||
{
|
||||
return $this->bookmarks()->whereNull('bookmark_folder_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new bookmark folder for the user.
|
||||
*/
|
||||
public function createBookmarkFolder(array $attributes = []): BookmarkFolder
|
||||
{
|
||||
return $this->bookmarkFolders()->create($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new bookmark for the user.
|
||||
*/
|
||||
public function createBookmark(array $attributes = []): Bookmark
|
||||
{
|
||||
return $this->bookmarks()->create($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bookmarks in a specific folder.
|
||||
*/
|
||||
public function bookmarksInFolder(BookmarkFolder $folder): HasMany
|
||||
{
|
||||
return $this->bookmarks()->where('bookmark_folder_id', $folder->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bookmarks in a specific folder by folder ID.
|
||||
*/
|
||||
public function bookmarksInFolderById(int $folderId): HasMany
|
||||
{
|
||||
return $this->bookmarks()->where('bookmark_folder_id', $folderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has any bookmarks.
|
||||
*/
|
||||
public function hasBookmarks(): bool
|
||||
{
|
||||
return $this->bookmarks()->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has any bookmark folders.
|
||||
*/
|
||||
public function hasBookmarkFolders(): bool
|
||||
{
|
||||
return $this->bookmarkFolders()->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total count of bookmarks for the user.
|
||||
*/
|
||||
public function getBookmarksCount(): int
|
||||
{
|
||||
return $this->bookmarks()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total count of bookmark folders for the user.
|
||||
*/
|
||||
public function getBookmarkFoldersCount(): int
|
||||
{
|
||||
return $this->bookmarkFolders()->count();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user