Adds basic address book handling; updates address book naming to use Book instead everywhere; removes AlpineJS; updates seeder to include address book and contact; updates DavController to handle address books and contacts now
This commit is contained in:
		
							parent
							
								
									cf8ee297d8
								
							
						
					
					
						commit
						5970991eac
					
				
							
								
								
									
										88
									
								
								app/Http/Controllers/BookController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								app/Http/Controllers/BookController.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,88 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use App\Models\Book;
 | 
			
		||||
use App\Models\BookMeta;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Support\Facades\Auth;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
 | 
			
		||||
class BookController extends Controller
 | 
			
		||||
{
 | 
			
		||||
    public function index()
 | 
			
		||||
    {
 | 
			
		||||
        $principal = 'principals/' . Auth::user()->email;
 | 
			
		||||
 | 
			
		||||
        $books = Book::query()
 | 
			
		||||
            ->join('addressbook_meta as meta', 'meta.addressbook_id', '=', 'addressbooks.id')
 | 
			
		||||
            ->where('principaluri', $principal)
 | 
			
		||||
            ->select('addressbooks.*', 'meta.color', 'meta.is_default')
 | 
			
		||||
            ->get();
 | 
			
		||||
 | 
			
		||||
        return view('books.index', compact('books'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function create()
 | 
			
		||||
    {
 | 
			
		||||
        return view('books.create');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function store(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $data = $request->validate([
 | 
			
		||||
            'displayname' => 'required|string|max:100',
 | 
			
		||||
            'color' => 'nullable|regex:/^#[0-9A-Fa-f]{6}$/',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $principal = 'principals/' . Auth::user()->email;
 | 
			
		||||
 | 
			
		||||
        $id = Book::insertGetId([
 | 
			
		||||
            'principaluri' => $principal,
 | 
			
		||||
            'uri' => (string) Str::uuid(),
 | 
			
		||||
            'displayname' => $data['displayname'],
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        BookMeta::create([
 | 
			
		||||
            'book_id' => $id,
 | 
			
		||||
            'color' => $data['color'] ?? '#cccccc',
 | 
			
		||||
            'is_default' => false,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        return redirect()->route('books.index');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function show(Book $book)
 | 
			
		||||
    {
 | 
			
		||||
        $this->authorize('view', $book);
 | 
			
		||||
 | 
			
		||||
        $book->load('meta', 'cards');
 | 
			
		||||
 | 
			
		||||
        return view('books.show', compact('book'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function edit(Book $book)
 | 
			
		||||
    {
 | 
			
		||||
        $book->load('meta');
 | 
			
		||||
 | 
			
		||||
        return view('books.edit', compact('book'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function update(Request $request, Book $book)
 | 
			
		||||
    {
 | 
			
		||||
        $data = $request->validate([
 | 
			
		||||
            'displayname' => 'required|string|max:100',
 | 
			
		||||
            'color' => 'nullable|regex:/^#[0-9A-Fa-f]{6}$/',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $book->update([
 | 
			
		||||
            'displayname' => $data['displayname'],
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $book->meta()->updateOrCreate([], [
 | 
			
		||||
            'color' => $data['color'] ?? '#cccccc',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        return redirect()->route('books.index')->with('toast', 'Address Book updated.');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -5,8 +5,10 @@ namespace App\Http\Controllers;
 | 
			
		||||
use Illuminate\Support\Facades\Auth;
 | 
			
		||||
use Illuminate\Support\Facades\Log;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
 | 
			
		||||
use App\Services\Dav\LaravelSabreAuthBackend;
 | 
			
		||||
use App\Services\Dav\LaravelSabrePrincipalBackend;
 | 
			
		||||
 | 
			
		||||
use Sabre\DAV;
 | 
			
		||||
use Sabre\DAV\Auth\Plugin as AuthPlugin;
 | 
			
		||||
use Sabre\DAVACL\Plugin as ACLPlugin;
 | 
			
		||||
@ -15,6 +17,8 @@ use Sabre\CalDAV\CalendarRoot;
 | 
			
		||||
use Sabre\CalDAV\Plugin as CalDavPlugin;
 | 
			
		||||
use Sabre\CalDAV\Backend\PDO as CalDAVPDO;
 | 
			
		||||
use Sabre\CardDAV\AddressBookRoot;
 | 
			
		||||
use Sabre\CardDAV\Plugin as CardDavPlugin;
 | 
			
		||||
use Sabre\CardDAV\Backend\PDO as CardDAVPDO;
 | 
			
		||||
 | 
			
		||||
class DavController extends Controller
 | 
			
		||||
{
 | 
			
		||||
@ -30,11 +34,12 @@ class DavController extends Controller
 | 
			
		||||
        $authBackend = new LaravelSabreAuthBackend();
 | 
			
		||||
        $principalBackend = new LaravelSabrePrincipalBackend();
 | 
			
		||||
        $calendarBackend = new CalDAVPDO($pdo);
 | 
			
		||||
        $cardBackend = new CardDAVPDO($pdo);
 | 
			
		||||
 | 
			
		||||
        $nodes = [
 | 
			
		||||
            new PrincipalCollection($principalBackend),
 | 
			
		||||
            new CalendarRoot($principalBackend, $calendarBackend)
 | 
			
		||||
            // Add your Calendars or Addressbooks here
 | 
			
		||||
            new CalendarRoot($principalBackend, $calendarBackend),
 | 
			
		||||
            new AddressBookRoot($principalBackend, $cardBackend)
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $server = new DAV\Server($nodes);
 | 
			
		||||
@ -43,6 +48,7 @@ class DavController extends Controller
 | 
			
		||||
        $server->addPlugin(new AuthPlugin($authBackend, 'Kithkin DAV'));
 | 
			
		||||
        $server->addPlugin(new ACLPlugin());
 | 
			
		||||
        $server->addPlugin(new CalDavPlugin());
 | 
			
		||||
        $server->addPlugin(new CardDavPlugin());
 | 
			
		||||
 | 
			
		||||
        $server->on('beforeMethod', function () {
 | 
			
		||||
            \Log::info('SabreDAV beforeMethod triggered');
 | 
			
		||||
 | 
			
		||||
@ -6,18 +6,18 @@ use Illuminate\Database\Eloquent\Model;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\HasMany;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\HasOne;
 | 
			
		||||
 | 
			
		||||
class AddressBook extends Model
 | 
			
		||||
class Book extends Model
 | 
			
		||||
{
 | 
			
		||||
    protected $table = 'addressbooks'; // Sabre table
 | 
			
		||||
    public $timestamps = false;
 | 
			
		||||
 | 
			
		||||
    public function contacts(): HasMany
 | 
			
		||||
    public function cards(): HasMany
 | 
			
		||||
    {
 | 
			
		||||
        return $this->hasMany(Contact::class, 'addressbookid');
 | 
			
		||||
        return $this->hasMany(Card::class, 'addressbookid');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function meta(): HasOne
 | 
			
		||||
    {
 | 
			
		||||
        return $this->hasOne(AddressbookMeta::class, 'addressbook_id');
 | 
			
		||||
        return $this->hasOne(BookMeta::class, 'addressbook_id');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -5,7 +5,7 @@ namespace App\Models;
 | 
			
		||||
use Illuminate\Database\Eloquent\Model;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 | 
			
		||||
 | 
			
		||||
class AddressBookMeta extends Model
 | 
			
		||||
class BookMeta extends Model
 | 
			
		||||
{
 | 
			
		||||
    protected $table = 'addressbook_meta';
 | 
			
		||||
    protected $primaryKey = 'addressbook_id';
 | 
			
		||||
@ -24,6 +24,6 @@ class AddressBookMeta extends Model
 | 
			
		||||
 | 
			
		||||
    public function addressBook(): BelongsTo
 | 
			
		||||
    {
 | 
			
		||||
        return $this->belongsTo(AddressBook::class, 'addressbook_id');
 | 
			
		||||
        return $this->belongsTo(Book::class, 'addressbook_id');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -6,19 +6,28 @@ use Illuminate\Database\Eloquent\Model;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\HasOne;
 | 
			
		||||
 | 
			
		||||
class Contact extends Model
 | 
			
		||||
class Card extends Model
 | 
			
		||||
{
 | 
			
		||||
    protected $table = 'cards'; // Sabre table
 | 
			
		||||
    public $timestamps = false;
 | 
			
		||||
    protected $primaryKey = 'id';
 | 
			
		||||
 | 
			
		||||
    public function addressBook(): BelongsTo
 | 
			
		||||
    protected $fillable = [
 | 
			
		||||
        'addressbookid',
 | 
			
		||||
        'uri',
 | 
			
		||||
        'lastmodified',
 | 
			
		||||
        'etag',
 | 
			
		||||
        'size',
 | 
			
		||||
        'carddata',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    public function book()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->belongsTo(AddressBook::class, 'addressbookid');
 | 
			
		||||
        return $this->belongsTo(Book::class, 'addressbookid');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function meta(): HasOne
 | 
			
		||||
    {
 | 
			
		||||
        return $this->hasOne(ContactMeta::class, 'card_id');
 | 
			
		||||
        return $this->hasOne(CardMeta::class, 'card_id');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -19,7 +19,7 @@ class ContactMeta extends Model
 | 
			
		||||
 | 
			
		||||
    public function contact(): BelongsTo
 | 
			
		||||
    {
 | 
			
		||||
        return $this->belongsTo(Contact::class, 'card_id');
 | 
			
		||||
        return $this->belongsTo(Card::class, 'card_id');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* convenience: return tags as array */
 | 
			
		||||
							
								
								
									
										30
									
								
								app/Policies/BookPolicy.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/Policies/BookPolicy.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Policies;
 | 
			
		||||
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
use App\Models\Book;
 | 
			
		||||
 | 
			
		||||
class BookPolicy
 | 
			
		||||
{
 | 
			
		||||
    public function view(User $user, Book $book): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $book->principaluri === 'principals/' . $user->email;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function update(User $user, Book $book): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $book->principaluri === 'principals/' . $user->email;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function delete(User $user, Book $book): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $book->principaluri === 'principals/' . $user->email;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function share(User $user, Book $book): bool
 | 
			
		||||
    {
 | 
			
		||||
        // Placeholder for future sharing logic
 | 
			
		||||
        return $book->principaluri === 'principals/' . $user->email;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -9,6 +9,7 @@
 | 
			
		||||
        "php": "^8.2",
 | 
			
		||||
        "laravel/framework": "^12.0",
 | 
			
		||||
        "laravel/tinker": "^2.10.1",
 | 
			
		||||
        "omnia-digital/livewire-calendar": "^3.2",
 | 
			
		||||
        "sabre/dav": "^4.7"
 | 
			
		||||
    },
 | 
			
		||||
    "require-dev": {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										151
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										151
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							@ -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": "5c25965fd8b365e18fdf5a50055d481f",
 | 
			
		||||
    "content-hash": "7376fcf2b57a1242a21836781b34e006",
 | 
			
		||||
    "packages": [
 | 
			
		||||
        {
 | 
			
		||||
            "name": "brick/math",
 | 
			
		||||
@ -2006,6 +2006,82 @@
 | 
			
		||||
            ],
 | 
			
		||||
            "time": "2024-12-08T08:18:47+00:00"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "livewire/livewire",
 | 
			
		||||
            "version": "v3.6.4",
 | 
			
		||||
            "source": {
 | 
			
		||||
                "type": "git",
 | 
			
		||||
                "url": "https://github.com/livewire/livewire.git",
 | 
			
		||||
                "reference": "ef04be759da41b14d2d129e670533180a44987dc"
 | 
			
		||||
            },
 | 
			
		||||
            "dist": {
 | 
			
		||||
                "type": "zip",
 | 
			
		||||
                "url": "https://api.github.com/repos/livewire/livewire/zipball/ef04be759da41b14d2d129e670533180a44987dc",
 | 
			
		||||
                "reference": "ef04be759da41b14d2d129e670533180a44987dc",
 | 
			
		||||
                "shasum": ""
 | 
			
		||||
            },
 | 
			
		||||
            "require": {
 | 
			
		||||
                "illuminate/database": "^10.0|^11.0|^12.0",
 | 
			
		||||
                "illuminate/routing": "^10.0|^11.0|^12.0",
 | 
			
		||||
                "illuminate/support": "^10.0|^11.0|^12.0",
 | 
			
		||||
                "illuminate/validation": "^10.0|^11.0|^12.0",
 | 
			
		||||
                "laravel/prompts": "^0.1.24|^0.2|^0.3",
 | 
			
		||||
                "league/mime-type-detection": "^1.9",
 | 
			
		||||
                "php": "^8.1",
 | 
			
		||||
                "symfony/console": "^6.0|^7.0",
 | 
			
		||||
                "symfony/http-kernel": "^6.2|^7.0"
 | 
			
		||||
            },
 | 
			
		||||
            "require-dev": {
 | 
			
		||||
                "calebporzio/sushi": "^2.1",
 | 
			
		||||
                "laravel/framework": "^10.15.0|^11.0|^12.0",
 | 
			
		||||
                "mockery/mockery": "^1.3.1",
 | 
			
		||||
                "orchestra/testbench": "^8.21.0|^9.0|^10.0",
 | 
			
		||||
                "orchestra/testbench-dusk": "^8.24|^9.1|^10.0",
 | 
			
		||||
                "phpunit/phpunit": "^10.4|^11.5",
 | 
			
		||||
                "psy/psysh": "^0.11.22|^0.12"
 | 
			
		||||
            },
 | 
			
		||||
            "type": "library",
 | 
			
		||||
            "extra": {
 | 
			
		||||
                "laravel": {
 | 
			
		||||
                    "aliases": {
 | 
			
		||||
                        "Livewire": "Livewire\\Livewire"
 | 
			
		||||
                    },
 | 
			
		||||
                    "providers": [
 | 
			
		||||
                        "Livewire\\LivewireServiceProvider"
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "autoload": {
 | 
			
		||||
                "files": [
 | 
			
		||||
                    "src/helpers.php"
 | 
			
		||||
                ],
 | 
			
		||||
                "psr-4": {
 | 
			
		||||
                    "Livewire\\": "src/"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "notification-url": "https://packagist.org/downloads/",
 | 
			
		||||
            "license": [
 | 
			
		||||
                "MIT"
 | 
			
		||||
            ],
 | 
			
		||||
            "authors": [
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Caleb Porzio",
 | 
			
		||||
                    "email": "calebporzio@gmail.com"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "description": "A front-end framework for Laravel.",
 | 
			
		||||
            "support": {
 | 
			
		||||
                "issues": "https://github.com/livewire/livewire/issues",
 | 
			
		||||
                "source": "https://github.com/livewire/livewire/tree/v3.6.4"
 | 
			
		||||
            },
 | 
			
		||||
            "funding": [
 | 
			
		||||
                {
 | 
			
		||||
                    "url": "https://github.com/livewire",
 | 
			
		||||
                    "type": "github"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "time": "2025-07-17T05:12:15+00:00"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "monolog/monolog",
 | 
			
		||||
            "version": "3.9.0",
 | 
			
		||||
@ -2507,6 +2583,79 @@
 | 
			
		||||
            ],
 | 
			
		||||
            "time": "2025-05-08T08:14:37+00:00"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "omnia-digital/livewire-calendar",
 | 
			
		||||
            "version": "3.2.0",
 | 
			
		||||
            "source": {
 | 
			
		||||
                "type": "git",
 | 
			
		||||
                "url": "https://github.com/omnia-digital/livewire-calendar.git",
 | 
			
		||||
                "reference": "9488ebaa84bf96f09c25dfbc2538d394aec365a7"
 | 
			
		||||
            },
 | 
			
		||||
            "dist": {
 | 
			
		||||
                "type": "zip",
 | 
			
		||||
                "url": "https://api.github.com/repos/omnia-digital/livewire-calendar/zipball/9488ebaa84bf96f09c25dfbc2538d394aec365a7",
 | 
			
		||||
                "reference": "9488ebaa84bf96f09c25dfbc2538d394aec365a7",
 | 
			
		||||
                "shasum": ""
 | 
			
		||||
            },
 | 
			
		||||
            "require": {
 | 
			
		||||
                "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
 | 
			
		||||
                "livewire/livewire": "^2.0||^3.0",
 | 
			
		||||
                "php": "^7.2|^8.0|^8.1|^8.2"
 | 
			
		||||
            },
 | 
			
		||||
            "require-dev": {
 | 
			
		||||
                "orchestra/testbench": "^5.0|^6.0",
 | 
			
		||||
                "phpunit/phpunit": "^8.0|^9.0|^10.0|^11.0|^12.0"
 | 
			
		||||
            },
 | 
			
		||||
            "type": "library",
 | 
			
		||||
            "extra": {
 | 
			
		||||
                "laravel": {
 | 
			
		||||
                    "aliases": {
 | 
			
		||||
                        "LivewireCalendar": "Omnia\\LivewireCalendar\\LivewireCalendarFacade"
 | 
			
		||||
                    },
 | 
			
		||||
                    "providers": [
 | 
			
		||||
                        "Omnia\\LivewireCalendar\\LivewireCalendarServiceProvider"
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "autoload": {
 | 
			
		||||
                "psr-4": {
 | 
			
		||||
                    "Omnia\\LivewireCalendar\\": "src"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "notification-url": "https://packagist.org/downloads/",
 | 
			
		||||
            "license": [
 | 
			
		||||
                "MIT"
 | 
			
		||||
            ],
 | 
			
		||||
            "authors": [
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Josh Torres",
 | 
			
		||||
                    "email": "josht@omniadigital.io",
 | 
			
		||||
                    "role": "Developer"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Andrés Santibáñez",
 | 
			
		||||
                    "email": "santibanez.andres@gmail.com",
 | 
			
		||||
                    "role": "Developer"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Osei Quashie",
 | 
			
		||||
                    "email": "osei@omniadigital.io",
 | 
			
		||||
                    "role": "Developer"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "description": "Laravel Livewire calendar component",
 | 
			
		||||
            "homepage": "https://github.com/omnia-digital/livewire-calendar",
 | 
			
		||||
            "keywords": [
 | 
			
		||||
                "livewire-calendar",
 | 
			
		||||
                "omnia",
 | 
			
		||||
                "omnia-digital"
 | 
			
		||||
            ],
 | 
			
		||||
            "support": {
 | 
			
		||||
                "issues": "https://github.com/omnia-digital/livewire-calendar/issues",
 | 
			
		||||
                "source": "https://github.com/omnia-digital/livewire-calendar/tree/3.2.0"
 | 
			
		||||
            },
 | 
			
		||||
            "time": "2025-02-28T18:18:23+00:00"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "phpoption/phpoption",
 | 
			
		||||
            "version": "1.9.3",
 | 
			
		||||
 | 
			
		||||
@ -101,5 +101,50 @@ ICS;
 | 
			
		||||
                'updated_at'  => now(),
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        /** create cards */
 | 
			
		||||
        $bookId = DB::table('addressbooks')->insertGetId([
 | 
			
		||||
            'principaluri' => $user->uri,
 | 
			
		||||
            'uri'          => 'default',
 | 
			
		||||
            'displayname'  => 'Default Address Book',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $vcard = <<<VCF
 | 
			
		||||
    BEGIN:VCARD
 | 
			
		||||
    VERSION:3.0
 | 
			
		||||
    FN:Seeded Contact
 | 
			
		||||
    EMAIL:seeded@example.com
 | 
			
		||||
    TEL:+1-555-123-4567
 | 
			
		||||
    UID:seeded-contact-001
 | 
			
		||||
    END:VCARD
 | 
			
		||||
    VCF;
 | 
			
		||||
 | 
			
		||||
        DB::table('addressbook_meta')->insert([
 | 
			
		||||
            'addressbook_id' => $bookId,
 | 
			
		||||
            'color'          => '#ff40ff',
 | 
			
		||||
            'is_default'     => true,
 | 
			
		||||
            'settings'       => null,
 | 
			
		||||
            'created_at'     => now(),
 | 
			
		||||
            'updated_at'     => now(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $cardId = DB::table('cards')->insertGetId([
 | 
			
		||||
            'addressbookid' => $bookId,
 | 
			
		||||
            'uri'           => Str::uuid().'.vcf',
 | 
			
		||||
            'lastmodified'  => now()->timestamp,
 | 
			
		||||
            'etag'          => md5($vcard),
 | 
			
		||||
            'size'          => strlen($vcard),
 | 
			
		||||
            'carddata'      => $vcard,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        DB::table('contact_meta')->insert([
 | 
			
		||||
            'card_id'    => $cardId,
 | 
			
		||||
            'avatar_url' => null,
 | 
			
		||||
            'tags'       => 'demo,seed',
 | 
			
		||||
            'notes'      => 'Seeded contact from DatabaseSeeder.',
 | 
			
		||||
            'created_at' => now(),
 | 
			
		||||
            'updated_at' => now(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										28
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -7,7 +7,6 @@
 | 
			
		||||
            "devDependencies": {
 | 
			
		||||
                "@tailwindcss/forms": "^0.5.2",
 | 
			
		||||
                "@tailwindcss/vite": "^4.0.0",
 | 
			
		||||
                "alpinejs": "^3.4.2",
 | 
			
		||||
                "autoprefixer": "^10.4.2",
 | 
			
		||||
                "axios": "^1.8.2",
 | 
			
		||||
                "concurrently": "^9.0.1",
 | 
			
		||||
@ -1196,33 +1195,6 @@
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "MIT"
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@vue/reactivity": {
 | 
			
		||||
            "version": "3.1.5",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
 | 
			
		||||
            "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "MIT",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@vue/shared": "3.1.5"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@vue/shared": {
 | 
			
		||||
            "version": "3.1.5",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
 | 
			
		||||
            "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "MIT"
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/alpinejs": {
 | 
			
		||||
            "version": "3.14.9",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.9.tgz",
 | 
			
		||||
            "integrity": "sha512-gqSOhTEyryU9FhviNqiHBHzgjkvtukq9tevew29fTj+ofZtfsYriw4zPirHHOAy9bw8QoL3WGhyk7QqCh5AYlw==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "MIT",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@vue/reactivity": "~3.1.1"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/ansi-regex": {
 | 
			
		||||
            "version": "6.1.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,6 @@
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@tailwindcss/forms": "^0.5.2",
 | 
			
		||||
        "@tailwindcss/vite": "^4.0.0",
 | 
			
		||||
        "alpinejs": "^3.4.2",
 | 
			
		||||
        "autoprefixer": "^10.4.2",
 | 
			
		||||
        "axios": "^1.8.2",
 | 
			
		||||
        "concurrently": "^9.0.1",
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1 @@
 | 
			
		||||
import './bootstrap';
 | 
			
		||||
 | 
			
		||||
import Alpine from 'alpinejs';
 | 
			
		||||
 | 
			
		||||
window.Alpine = Alpine;
 | 
			
		||||
 | 
			
		||||
Alpine.start();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								resources/views/books/create.blade.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								resources/views/books/create.blade.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
<x-layout>
 | 
			
		||||
    <h1>Create Address Book</h1>
 | 
			
		||||
    @include('addressbooks.form', ['action' => route('addressbooks.store'), 'isEdit' => false])
 | 
			
		||||
</x-layout>
 | 
			
		||||
							
								
								
									
										4
									
								
								resources/views/books/edit.blade.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								resources/views/books/edit.blade.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
<x-layout>
 | 
			
		||||
    <h1>Edit Address Book</h1>
 | 
			
		||||
    @include('addressbooks.form', ['action' => route('addressbooks.update', $addressbook), 'isEdit' => true])
 | 
			
		||||
</x-layout>
 | 
			
		||||
							
								
								
									
										12
									
								
								resources/views/books/form.blade.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								resources/views/books/form.blade.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
<form method="POST" action="{{ $action }}">
 | 
			
		||||
    @csrf
 | 
			
		||||
    @if($isEdit) @method('PUT') @endif
 | 
			
		||||
 | 
			
		||||
    <label>Display Name:</label>
 | 
			
		||||
    <input name="displayname" value="{{ old('displayname', $addressbook->displayname ?? '') }}" required>
 | 
			
		||||
 | 
			
		||||
    <label>Color:</label>
 | 
			
		||||
    <input name="color" type="color" value="{{ old('color', $addressbook->meta->color ?? '#cccccc') }}">
 | 
			
		||||
 | 
			
		||||
    <button type="submit">{{ $isEdit ? 'Update' : 'Create' }}</button>
 | 
			
		||||
</form>
 | 
			
		||||
							
								
								
									
										28
									
								
								resources/views/books/index.blade.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								resources/views/books/index.blade.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
<x-app-layout>
 | 
			
		||||
    <x-slot name="header">
 | 
			
		||||
        <h2 class="text-xl font-semibold leading-tight">
 | 
			
		||||
            {{ __('My Address Books') }}
 | 
			
		||||
        </h2>
 | 
			
		||||
    </x-slot>
 | 
			
		||||
 | 
			
		||||
    <div class="py-6">
 | 
			
		||||
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
 | 
			
		||||
 | 
			
		||||
            {{-- Books list --}}
 | 
			
		||||
            <div class="bg-white shadow sm:rounded-lg">
 | 
			
		||||
                <ul class="divide-y divide-gray-200">
 | 
			
		||||
                    @forelse($books as $book)
 | 
			
		||||
                        <li class="px-6 py-4 flex items-center justify-between">
 | 
			
		||||
                            <a href="{{ route('books.show', $book) }}" class="font-medium text-indigo-600">
 | 
			
		||||
                                {{ $book->displayname }}
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    @empty
 | 
			
		||||
                        <li class="px-6 py-4">{{ __('No address books yet.') }}</li>
 | 
			
		||||
                    @endforelse
 | 
			
		||||
                </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</x-app-layout>
 | 
			
		||||
							
								
								
									
										15
									
								
								resources/views/books/show.blade.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								resources/views/books/show.blade.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
<x-app-layout>
 | 
			
		||||
    <h1>{{ $book->displayname }}</h1>
 | 
			
		||||
 | 
			
		||||
    <p>Color: <span style="color: {{ $book->meta->color }}">{{ $book->meta->color }}</span></p>
 | 
			
		||||
 | 
			
		||||
    <h2>Contacts</h2>
 | 
			
		||||
    <ul>
 | 
			
		||||
        @foreach ($book->cards as $card)
 | 
			
		||||
            <li>{{ $card->displayname ?? 'Unnamed contact' }}</li>
 | 
			
		||||
        @endforeach
 | 
			
		||||
    </ul>
 | 
			
		||||
 | 
			
		||||
    <a href="{{ route('books.edit', $book) }}">Edit Book</a>
 | 
			
		||||
    <a href="{{ route('books.index') }}">Back to all</a>
 | 
			
		||||
</x-app-layout>
 | 
			
		||||
@ -7,10 +7,6 @@
 | 
			
		||||
 | 
			
		||||
        <title>{{ config('app.name', 'Laravel') }}</title>
 | 
			
		||||
 | 
			
		||||
        <!-- Fonts -->
 | 
			
		||||
        <link rel="preconnect" href="https://fonts.bunny.net">
 | 
			
		||||
        <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
 | 
			
		||||
 | 
			
		||||
        <!-- Scripts -->
 | 
			
		||||
        @vite(['resources/css/app.css', 'resources/js/app.js'])
 | 
			
		||||
    </head>
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,11 @@
 | 
			
		||||
                        {{ __('Calendars') }}
 | 
			
		||||
                    </x-nav-link>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
 | 
			
		||||
                    <x-nav-link :href="route('books.index')" :active="request()->routeIs('books*')">
 | 
			
		||||
                        {{ __('Address Books') }}
 | 
			
		||||
                    </x-nav-link>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Settings Dropdown -->
 | 
			
		||||
 | 
			
		||||
@ -2,9 +2,11 @@
 | 
			
		||||
 | 
			
		||||
use Illuminate\Support\Facades\Route;
 | 
			
		||||
use App\Http\Controllers\ProfileController;
 | 
			
		||||
use App\Http\Controllers\BookController;
 | 
			
		||||
use App\Http\Controllers\CalendarController;
 | 
			
		||||
use App\Http\Controllers\EventController;
 | 
			
		||||
use App\Http\Controllers\CardController;
 | 
			
		||||
use App\Http\Controllers\DavController;
 | 
			
		||||
use App\Http\Controllers\EventController;
 | 
			
		||||
use App\Http\Controllers\SubscriptionController;
 | 
			
		||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
 | 
			
		||||
 | 
			
		||||
@ -44,6 +46,19 @@ Route::middleware('auth')->group(function () {
 | 
			
		||||
            Route::get ('events/{event}/edit',  [EventController::class, 'edit'  ])->name('events.edit');
 | 
			
		||||
            Route::put ('events/{event}',       [EventController::class, 'update'])->name('events.update');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    /** address books */
 | 
			
		||||
    Route::resource('books', BookController::class)
 | 
			
		||||
        ->names('books')                       // books.index, books.create, …
 | 
			
		||||
        ->parameter('books', 'book');          // {book} binding
 | 
			
		||||
 | 
			
		||||
    /** contacts inside a book
 | 
			
		||||
        nested so urls look like /books/{book}/contacts/{contact} */
 | 
			
		||||
    Route::resource('books.contacts', ContactController::class)
 | 
			
		||||
        ->names('books.contacts')
 | 
			
		||||
        ->parameter('contacts', 'contact')
 | 
			
		||||
        ->except(['index'])                    // you may add an index later
 | 
			
		||||
        ->shallow();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/* Breeze auth routes (login, register, password reset, etc.) */
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user