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