diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index c231e99..9169616 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -20,12 +20,12 @@ class BookController extends Controller ->select('addressbooks.*', 'meta.color', 'meta.is_default') ->get(); - return view('books.index', compact('books')); + return view('book.index', compact('books')); } public function create() { - return view('books.create'); + return view('book.create'); } public function store(Request $request) @@ -49,7 +49,7 @@ class BookController extends Controller 'is_default' => false, ]); - return redirect()->route('books.index'); + return redirect()->route('book.index'); } public function show(Book $book) @@ -58,14 +58,14 @@ class BookController extends Controller $book->load('meta', 'cards'); - return view('books.show', compact('book')); + return view('book.show', compact('book')); } public function edit(Book $book) { $book->load('meta'); - return view('books.edit', compact('book')); + return view('book.edit', compact('book')); } public function update(Request $request, Book $book) @@ -83,6 +83,6 @@ class BookController extends Controller 'color' => $data['color'] ?? '#cccccc', ]); - return redirect()->route('books.index')->with('toast', 'Address Book updated.'); + return redirect()->route('book.index')->with('toast', 'Address Book updated.'); } } diff --git a/app/Http/Controllers/CalendarController.php b/app/Http/Controllers/CalendarController.php index 52c23c1..f2580a7 100644 --- a/app/Http/Controllers/CalendarController.php +++ b/app/Http/Controllers/CalendarController.php @@ -11,6 +11,7 @@ use App\Models\Calendar; use App\Models\CalendarMeta; use App\Models\CalendarInstance; use App\Models\Event; +use App\Models\EventMeta; class CalendarController extends Controller { @@ -35,12 +36,17 @@ class CalendarController extends Controller // get the view and time range [$view, $range] = $this->resolveRange($request); + // get the user's selected calendars + $visible = collect($request->query('c', [])); + // load the user's calendars $calendars = Calendar::query() ->select( 'calendars.id', 'ci.displayname', 'ci.calendarcolor', + 'ci.uri as slug', + 'ci.timezone as timezone', 'meta.color as meta_color', 'meta.color_fg as meta_color_fg' ) @@ -48,54 +54,75 @@ class CalendarController extends Controller ->leftJoin('calendar_meta as meta', 'meta.calendar_id', '=', 'calendars.id') ->where('ci.principaluri', $principal) ->orderBy('ci.displayname') - ->get(); + ->get() + ->map(function ($cal) use ($visible) { + $cal->visible = $visible->isEmpty() || $visible->contains($cal->slug); + return $cal; + }); + + // handy lookup: [id => calendar row] + $calendar_map = $calendars->keyBy('id'); // get all the events in one query $events = Event::forCalendarsInRange( $calendars->pluck('id'), $range['start'], $range['end'] - ); + )->map(function ($e) use ($calendar_map) { + + // event's calendar + $cal = $calendar_map[$e->calendarid]; + + // get utc dates from the database + $start_utc = $e->meta->start_at ?? + Carbon::createFromTimestamp($e->firstoccurence); + $end_utc = $e->meta->end_at ?? + ($e->lastoccurence ? Carbon::createFromTimestamp($e->lastoccurence) : null); + + // convert to calendar timezone + $timezone = $calendar_map[$e->calendarid]->timezone ?? config('app.timezone'); + $start_local = $start_utc->copy()->timezone($timezone); + $end_local = optional($end_utc)->copy()->timezone($timezone); + + // return events array + return [ + 'id' => $e->id, + 'calendar_id' => $e->calendarid, + 'calendar_slug' => $cal->slug, + 'title' => $e->meta->title ?? '(no title)', + 'start' => $start_utc->toIso8601String(), + 'end' => optional($end_utc)->toIso8601String(), + 'start_ui' => $start_local->format('g:ia'), + 'end_ui' => optional($end_local)->format('g:ia'), + 'timezone' => $timezone, + 'visible' => $cal->visible, + ]; + })->keyBy('id'); // create the calendar grid of days $grid = $this->buildCalendarGrid($view, $range, $events); - // format the data for the frontend + // format the data for the frontend, including separate arrays for events specifically and the big grid $payload = [ - 'view' => $view, - 'range' => $range, + 'view' => $view, + 'range' => $range, 'active' => [ 'year' => $range['start']->format('Y'), 'month' => $range['start']->format("F"), 'day' => $range['start']->format("d"), ], - 'calendars' => $calendars->keyBy('id')->map(function ($cal) { + 'calendars' => $calendar_map->map(function ($cal) { return [ 'id' => $cal->id, + 'slug' => $cal->slug, 'name' => $cal->displayname, 'color' => $cal->meta_color ?? $cal->calendarcolor ?? '#1a1a1a', // clean this up @todo 'color_fg' => $cal->meta_color_fg ?? '#ffffff', // clean this up - 'on' => true, // default to visible; the UI can toggle this + 'visible' => true, // default to visible; the UI can toggle this ]; }), - 'events' => $events->map(function ($e) { // just the events map - // fall back to Sabre timestamps if meta is missing - $start = $e->meta->start_at - ?? Carbon::createFromTimestamp($e->firstoccurence); - $end = $e->meta->end_at - ?? ($e->lastoccurence ? Carbon::createFromTimestamp($e->lastoccurence) : null); - - return [ - 'id' => $e->id, - 'calendar_id' => $e->calendarid, - 'title' => $e->meta->title ?? '(no title)', - 'start' => $start->format('c'), - 'start_ui' => $start->format('g:ia'), - 'end' => optional($end)->format('c'), - 'end_ui' => optional($end)->format('g:ia') - ]; - }), - 'grid' => $grid, + 'events' => $events, // keyed, one copy each + 'grid' => $grid, // day objects hold only ID-sets ]; return view('calendar.index', $payload); @@ -283,9 +310,6 @@ class CalendarController extends Controller return [$view, ['start' => $start, 'end' => $end]]; } - - - /** * Assemble an array of day-objects for the requested view. * @@ -303,71 +327,53 @@ class CalendarController extends Controller private function buildCalendarGrid(string $view, array $range, Collection $events): array { // index events by YYYY-MM-DD for quick lookup */ - $eventsByDay = []; + $events_by_day = []; foreach ($events as $ev) { - $start = $ev->meta->start_at - ?? Carbon::createFromTimestamp($ev->firstoccurence); - $end = $ev->meta->end_at - ?? ($ev->lastoccurence - ? Carbon::createFromTimestamp($ev->lastoccurence) - : $start); + $start = Carbon::parse($ev['start'])->tz($ev['timezone']); + $end = $ev['end'] ? Carbon::parse($ev['end'])->tz($ev['timezone']) : $start; - // spread multi-day events across each day they touch + // spread multi-day events for ($d = $start->copy()->startOfDay(); $d->lte($end->copy()->endOfDay()); $d->addDay()) { - $key = $d->toDateString(); // e.g. '2025-07-14' - $eventsByDay[$key] ??= []; - $eventsByDay[$key][] = [ - 'id' => $ev->id, - 'calendar_id' => $ev->calendarid, - 'title' => $ev->meta->title ?? '(no title)', - 'start' => $start->format('c'), - 'start_ui' => $start->format('g:ia'), - 'end' => optional($end)->format('c'), - 'end_ui' => optional($end)->format('g:ia') - ]; + $key = $d->toDateString(); + $events_by_day[$key][] = $ev['id']; } } - // determine which individual days belong to this view */ + // determine span of days for the selected view switch ($view) { case 'week': - $gridStart = $range['start']->copy(); - $gridEnd = $range['start']->copy()->addDays(6); + $grid_start = $range['start']->copy(); + $grid_end = $range['start']->copy()->addDays(6); break; case '4day': - $gridStart = $range['start']->copy(); - $gridEnd = $range['start']->copy()->addDays(3); + $grid_start = $range['start']->copy(); + $grid_end = $range['start']->copy()->addDays(3); break; - default: // month - $gridStart = $range['start']->copy()->startOfWeek(); // Sunday-start; tweak if needed - $gridEnd = $range['end']->copy()->endOfWeek(); + default: /* month */ + $grid_start = $range['start']->copy()->startOfWeek(); // Sunday start + $grid_end = $range['end']->copy()->endOfWeek(); } - // walk the span, build the day objects */ + // view span bounds and build day objects $days = []; - for ($day = $gridStart->copy(); $day->lte($gridEnd); $day->addDay()) { - $iso = $day->toDateString(); - $isToday = $day->isSameDay(Carbon::today()); - $days[] = [ - 'date' => $iso, - 'label' => $day->format('j'), - 'in_month' => $day->month === $range['start']->month, - 'is_today' => $isToday, - 'events' => $eventsByDay[$iso] ?? [], - ]; + for ($day = $grid_start->copy(); $day->lte($grid_end); $day->addDay()) { + $iso = $day->toDateString(); + $days[] = [ + 'date' => $iso, + 'label' => $day->format('j'), + 'in_month' => $day->month === $range['start']->month, + 'is_today' => $day->isSameDay(Carbon::today()), + 'events' => array_fill_keys($events_by_day[$iso] ?? [], true), + ]; } - // for a month view, also group into weeks - if ($view === 'month') { - $weeks = array_chunk($days, 7); // 7 days per week row - return ['days' => $days, 'weeks' => $weeks]; - } - - return ['days' => $days]; + return $view === 'month' + ? ['days' => $days, 'weeks' => array_chunk($days, 7)] + : ['days' => $days]; } } diff --git a/app/Http/Controllers/DavController.php b/app/Http/Controllers/DavController.php index 3e60d72..403c24e 100644 --- a/app/Http/Controllers/DavController.php +++ b/app/Http/Controllers/DavController.php @@ -16,6 +16,7 @@ use Sabre\DAVACL\PrincipalCollection; use Sabre\CalDAV\CalendarRoot; use Sabre\CalDAV\Plugin as CalDavPlugin; use Sabre\CalDAV\Backend\PDO as CalDAVPDO; +use Sabre\CalDAV\Subscriptions\Plugin as SubscriptionsPlugin; use Sabre\CardDAV\AddressBookRoot; use Sabre\CardDAV\Plugin as CardDavPlugin; use Sabre\CardDAV\Backend\PDO as CardDAVPDO; @@ -49,6 +50,7 @@ class DavController extends Controller $server->addPlugin(new ACLPlugin()); $server->addPlugin(new CalDavPlugin()); $server->addPlugin(new CardDavPlugin()); + $server->addPlugin(new SubscriptionsPlugin()); $server->on('beforeMethod', function () { \Log::info('SabreDAV beforeMethod triggered'); diff --git a/app/Http/Controllers/EventController.php b/app/Http/Controllers/EventController.php index a4d2762..e7a90fc 100644 --- a/app/Http/Controllers/EventController.php +++ b/app/Http/Controllers/EventController.php @@ -33,9 +33,72 @@ class EventController extends Controller $start = $event->start_at; $end = $event->end_at; - return view('events.form', compact('calendar', 'instance', 'event', 'start', 'end')); + return view('event.form', compact('calendar', 'instance', 'event', 'start', 'end')); } + /** + * edit event page + */ + public function edit(Calendar $calendar, Event $event) + { + $this->authorize('update', $calendar); + + $instance = $calendar->instanceForUser(); + $timezone = $instance?->timezone ?? 'UTC'; + + $start = optional($event->meta?->start_at) + ?->timezone($timezone) + ?->format('Y-m-d\TH:i'); + + $end = optional($event->meta?->end_at) + ?->timezone($timezone) + ?->format('Y-m-d\TH:i'); + + return view('event.form', compact('calendar', 'instance', 'event', 'start', 'end')); + } + + /** + * single event view handling + * + * URL: /calendar/{uuid}/event/{event_id} + */ + public function show(Request $request, Calendar $calendar, Event $event) + { + // ensure the event really belongs to the parent calendar + if ((int) $event->calendarid !== (int) $calendar->id) { + abort(Response::HTTP_NOT_FOUND); + } + + // authorize + $this->authorize('view', $event); + + // eager-load meta so the view has everything + $event->load('meta'); + + // check for HTML; it sends `HX-Request: true` on every AJAX call + $isHtmx = $request->header('HX-Request') === 'true'; + + // convert Sabre timestamps if meta is missing + $start = $event->meta->start_at + ?? Carbon::createFromTimestamp($event->firstoccurence); + $end = $event->meta->end_at + ?? ($event->lastoccurence + ? Carbon::createFromTimestamp($event->lastoccurence) + : $start); + + $data = compact('calendar', 'event', 'start', 'end'); + + return $isHtmx + ? view('event.partials.details', $data) // tiny fragment for the modal + : view('event.show', $data); // full-page fallback + } + + + /** + * BACKEND METHODS + * + */ + /** * insert vevent into sabre’s calendarobjects + meta row */ @@ -108,28 +171,7 @@ ICS; 'end_at' => $end, ]); - return redirect()->route('calendars.show', $calendar); - } - - /** - * show the event edit form - */ - public function edit(Calendar $calendar, Event $event) - { - $this->authorize('update', $calendar); - - $instance = $calendar->instanceForUser(); - $timezone = $instance?->timezone ?? 'UTC'; - - $start = optional($event->meta?->start_at) - ?->timezone($timezone) - ?->format('Y-m-d\TH:i'); - - $end = optional($event->meta?->end_at) - ?->timezone($timezone) - ?->format('Y-m-d\TH:i'); - - return view('events.form', compact('calendar', 'instance', 'event', 'start', 'end')); + return redirect()->route('calendar.show', $calendar); } /** @@ -196,6 +238,6 @@ ICS; 'end_at' => $end, ]); - return redirect()->route('calendars.show', $calendar); + return redirect()->route('calendar.show', $calendar); } } diff --git a/app/Http/Controllers/IcsController.php b/app/Http/Controllers/IcsController.php new file mode 100644 index 0000000..670dd2a --- /dev/null +++ b/app/Http/Controllers/IcsController.php @@ -0,0 +1,67 @@ +firstOrFail(); + + $calendar = $instance->calendar()->with(['events.meta'])->firstOrFail(); + $timezone = $instance->timezone ?? 'UTC'; + + $ical = $this->generateICalendarFeed($calendar->events, $timezone); + + return Response::make($ical, 200, [ + 'Content-Type' => 'text/calendar; charset=utf-8', + 'Content-Disposition' => 'inline; filename="' . $calendarUri . '.ics"', + ]); + } + + protected function generateICalendarFeed($events, string $tz): string + { + $output = []; + $output[] = 'BEGIN:VCALENDAR'; + $output[] = 'VERSION:2.0'; + $output[] = 'PRODID:-//Kithkin Calendar//EN'; + $output[] = 'CALSCALE:GREGORIAN'; + $output[] = 'METHOD:PUBLISH'; + + foreach ($events as $event) { + $meta = $event->meta; + + if (!$meta || !$meta->start_at || !$meta->end_at) { + continue; + } + + $start = Carbon::parse($meta->start_at)->timezone($tz)->format('Ymd\THis'); + $end = Carbon::parse($meta->end_at)->timezone($tz)->format('Ymd\THis'); + + $output[] = 'BEGIN:VEVENT'; + $output[] = 'UID:' . $event->uid; + $output[] = 'SUMMARY:' . $this->escape($meta->title ?? '(Untitled)'); + $output[] = 'DESCRIPTION:' . $this->escape($meta->description ?? ''); + $output[] = 'DTSTART;TZID=' . $tz . ':' . $start; + $output[] = 'DTEND;TZID=' . $tz . ':' . $end; + $output[] = 'DTSTAMP:' . Carbon::parse($event->lastmodified)->format('Ymd\THis\Z'); + if ($meta->location) { + $output[] = 'LOCATION:' . $this->escape($meta->location); + } + $output[] = 'END:VEVENT'; + } + + $output[] = 'END:VCALENDAR'; + + return implode("\r\n", $output); + } + + protected function escape(?string $text): string + { + return str_replace(['\\', ';', ',', "\n"], ['\\\\', '\;', '\,', '\n'], $text ?? ''); + } +} diff --git a/app/Http/Controllers/SubscriptionController.php b/app/Http/Controllers/SubscriptionController.php index b8cbdcf..0884d76 100644 --- a/app/Http/Controllers/SubscriptionController.php +++ b/app/Http/Controllers/SubscriptionController.php @@ -2,66 +2,82 @@ namespace App\Http\Controllers; -use App\Models\CalendarInstance; -use Illuminate\Support\Facades\Response; -use Carbon\Carbon; +use Illuminate\Http\Request; +use Illuminate\Support\Str; +use App\Models\Subscription; class SubscriptionController extends Controller { - public function download(string $calendarUri) + public function index() { - $instance = CalendarInstance::where('uri', $calendarUri)->firstOrFail(); + $subs = Subscription::where( + 'principaluri', + auth()->user()->principal_uri + )->get(); - $calendar = $instance->calendar()->with(['events.meta'])->firstOrFail(); - $timezone = $instance->timezone ?? 'UTC'; + return view('subscription.index', compact('subs')); + } - $ical = $this->generateICalendarFeed($calendar->events, $timezone); + public function create() + { + return view('subscription.create'); + } - return Response::make($ical, 200, [ - 'Content-Type' => 'text/calendar; charset=utf-8', - 'Content-Disposition' => 'inline; filename="' . $calendarUri . '.ics"', + public function store(Request $request) + { + $data = $request->validate([ + 'source' => 'required|url', + 'displayname' => 'nullable|string|max:255', + 'calendarcolor' => 'nullable|regex:/^#[0-9A-Fa-f]{6}$/', + 'refreshrate' => 'nullable|string|max:10', ]); + + Subscription::create([ + 'uri' => Str::uuid(), // unique per principal + 'principaluri' => auth()->user()->principal_uri, + //...$data, + 'source' => $data['source'], + 'displayname' => $data['displayname'] ?? null, + 'calendarcolor' => $data['calendarcolor'] ?? null, + 'refreshrate' => $data['refreshrate'] ?? null, + 'striptodos' => false, + 'stripalarms' => false, + 'stripattachments' => false, + ]); + + return redirect()->route('subscription.index') + ->with('toast', __('Subscription added!')); } - protected function generateICalendarFeed($events, string $tz): string + public function edit(Subscription $subscription) { - $output = []; - $output[] = 'BEGIN:VCALENDAR'; - $output[] = 'VERSION:2.0'; - $output[] = 'PRODID:-//Kithkin Calendar//EN'; - $output[] = 'CALSCALE:GREGORIAN'; - $output[] = 'METHOD:PUBLISH'; - - foreach ($events as $event) { - $meta = $event->meta; - - if (!$meta || !$meta->start_at || !$meta->end_at) { - continue; - } - - $start = Carbon::parse($meta->start_at)->timezone($tz)->format('Ymd\THis'); - $end = Carbon::parse($meta->end_at)->timezone($tz)->format('Ymd\THis'); - - $output[] = 'BEGIN:VEVENT'; - $output[] = 'UID:' . $event->uid; - $output[] = 'SUMMARY:' . $this->escape($meta->title ?? '(Untitled)'); - $output[] = 'DESCRIPTION:' . $this->escape($meta->description ?? ''); - $output[] = 'DTSTART;TZID=' . $tz . ':' . $start; - $output[] = 'DTEND;TZID=' . $tz . ':' . $end; - $output[] = 'DTSTAMP:' . Carbon::parse($event->lastmodified)->format('Ymd\THis\Z'); - if ($meta->location) { - $output[] = 'LOCATION:' . $this->escape($meta->location); - } - $output[] = 'END:VEVENT'; - } - - $output[] = 'END:VCALENDAR'; - - return implode("\r\n", $output); + $this->authorize('update', $subscription); + return view('subscription.edit', ['subscription' => $subscription]); } - protected function escape(?string $text): string + public function update(Request $request, Subscription $subscription) { - return str_replace(['\\', ';', ',', "\n"], ['\\\\', '\;', '\,', '\n'], $text ?? ''); + $this->authorize('update', $subscription); + + $data = $request->validate([ + 'displayname' => 'nullable|string|max:255', + 'calendarcolor' => 'nullable|regex:/^#[0-9A-Fa-f]{6}$/', + 'refreshrate' => 'nullable|string|max:10', + 'striptodos' => 'sometimes|boolean', + 'stripalarms' => 'sometimes|boolean', + 'stripattachments' => 'sometimes|boolean', + ]); + + $subscription->update($data); + + return back()->with('toast', __('Subscription updated!')); + } + + public function destroy(Subscription $subscription) + { + $this->authorize('delete', $subscription); + $subscription->delete(); + + return back()->with('toast', __('Subscription removed!')); } } diff --git a/app/Models/Calendar.php b/app/Models/Calendar.php index fc762b1..437642f 100644 --- a/app/Models/Calendar.php +++ b/app/Models/Calendar.php @@ -2,9 +2,11 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; +use Illuminate\Support\Facades\Auth; class Calendar extends Model { @@ -44,4 +46,20 @@ class Calendar extends Model ->where('principaluri', 'principals/' . $user->email) ->first(); } + + /** + * convert "/calendar/{slug}" into the correct calendar instance (uri column) + * + * @param mixed $value The URI segment (instance UUID). + * @param string|null $field Ignored in our override. + */ + public function resolveRouteBinding($value, $field = null): mixed + { + return $this->whereHas('instances', function (Builder $q) use ($value) { + $q->where('uri', $value) + ->where('principaluri', Auth::user()->principal_uri); + }) + ->with('instances') + ->firstOrFail(); + } } diff --git a/app/Models/CalendarMeta.php b/app/Models/CalendarMeta.php index 0487ae7..0e04ec0 100644 --- a/app/Models/CalendarMeta.php +++ b/app/Models/CalendarMeta.php @@ -24,6 +24,12 @@ class CalendarMeta extends Model 'settings' => 'array', ]; + protected static function boot() + { + parent::boot(); + // + } + public function calendar(): BelongsTo { return $this->belongsTo(Calendar::class, 'calendar_id'); diff --git a/app/Models/CardMeta.php b/app/Models/CardMeta.php index 518f83e..86c8de0 100644 --- a/app/Models/CardMeta.php +++ b/app/Models/CardMeta.php @@ -5,7 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -class ContactMeta extends Model +class CardMeta extends Model { protected $table = 'contact_meta'; protected $primaryKey = 'card_id'; diff --git a/app/Models/Event.php b/app/Models/Event.php index 48579ab..e4b5193 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -24,12 +24,6 @@ class Event extends Model 'calendardata', ]; - /* casts */ - protected $casts = [ - 'start_at' => 'datetime', - 'end_at' => 'datetime', - ]; - /* owning calendar */ public function calendar(): BelongsTo { diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php new file mode 100644 index 0000000..e0c46f9 --- /dev/null +++ b/app/Models/Subscription.php @@ -0,0 +1,31 @@ + 'bool', + 'stripalarms' => 'bool', + 'stripattachments' => 'bool', + ]; +} diff --git a/app/Policies/EventPolicy.php b/app/Policies/EventPolicy.php new file mode 100644 index 0000000..c056265 --- /dev/null +++ b/app/Policies/EventPolicy.php @@ -0,0 +1,70 @@ +instances() + ->where('principaluri', 'principals/'.$user->email) + ->exists(); + } + + /* ------------------------------------------------- + | List all events (e.g. /calendar/{id}/events) + |-------------------------------------------------*/ + public function viewAny(User $user): bool + { + return true; // authenticated users can query their events + } + + /* ------------------------------------------------- + | Show a single event (/calendar/{id}/event/{event}) + |-------------------------------------------------*/ + public function view(User $user, Event $event): bool + { + return $this->ownsCalendar($user, $event->calendar); + } + + /* ------------------------------------------------- + | Create an event (needs parent calendar) + |-------------------------------------------------*/ + public function create(User $user, Calendar $calendar): bool + { + return $this->ownsCalendar($user, $calendar); + } + + /* ------------------------------------------------- + | Update / delete use same ownership rule + |-------------------------------------------------*/ + public function update(User $user, Event $event): bool + { + return $this->view($user, $event); + } + + public function delete(User $user, Event $event): bool + { + return $this->view($user, $event); + } + + /* ------------------------------------------------- + | Not supported + |-------------------------------------------------*/ + public function restore(User $user, Event $event): bool + { + return false; + } + + public function forceDelete(User $user, Event $event): bool + { + return false; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ea53523..2bad5dc 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -8,7 +8,7 @@ use Illuminate\Support\Facades\Blade; class AppServiceProvider extends ServiceProvider { /** - * Register any application services. + * register any application services */ public function register(): void { @@ -16,7 +16,7 @@ class AppServiceProvider extends ServiceProvider } /** - * Bootstrap any application services. + * bootstrap any application services */ public function boot(): void { diff --git a/database/migrations/2025_07_15_000001_create_calendar_meta_table.php b/database/migrations/2025_07_15_000001_create_calendar_meta_table.php index 1fb4aaa..74977cf 100644 --- a/database/migrations/2025_07_15_000001_create_calendar_meta_table.php +++ b/database/migrations/2025_07_15_000001_create_calendar_meta_table.php @@ -9,7 +9,11 @@ return new class extends Migration public function up(): void { Schema::create('calendar_meta', function (Blueprint $table) { + + // // FK = PK to Sabre’s calendars.id $table->unsignedInteger('calendar_id')->primary(); // FK = PK + + // UI fields $table->string('title')->nullable(); // ui override $table->string('color', 7)->nullable(); // bg color $table->string('color_fg', 7)->nullable(); // fg color diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index e977266..5647a8b 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -13,14 +13,19 @@ class DatabaseSeeder extends Seeder { public function run(): void { - /** credentials from .env (with sensible fall-backs) */ + /** + * + * admin users + */ + + // credentials from .env (with sensible fall-backs) $email = env('ADMIN_EMAIL', 'admin@example.com'); $password = env('ADMIN_PASSWORD', 'changeme'); $firstname = env('ADMIN_FIRSTNAME', 'Admin'); $lastname = env('ADMIN_LASTNAME', 'Account'); $timezone = env('APP_TIMEZONE', 'UTC'); - /** create or update the admin user */ + // create or update the admin user $user = User::updateOrCreate( ['email' => $email], [ @@ -31,13 +36,18 @@ class DatabaseSeeder extends Seeder ] ); - /** fill the sabre-friendly columns */ + // fill the sabre-friendly columns $user->update([ 'uri' => 'principals/'.$user->email, 'displayname' => $firstname.' '.$lastname, ]); - /** sample caldav data */ + /** + * + * calendar and meta + */ + + // sample caldav data $calId = DB::table('calendars')->insertGetId([ 'synctoken' => 1, 'components' => 'VEVENT', @@ -50,63 +60,107 @@ class DatabaseSeeder extends Seeder 'displayname' => 'Sample Calendar', 'description' => 'Seeded calendar', 'calendarorder' => 0, - 'calendarcolor' => '#007db6', - 'timezone' => config('app.timezone', 'UTC'), + 'calendarcolor' => '#0038ff', + 'timezone' => $timezone, ]); DB::table('calendar_meta')->updateOrInsert( - ['calendar_id' => $instanceId], - ['color' => '#007AFF'] + ['calendar_id' => $calId], // @todo should this be calendar id or instance id? + ['color' => '#0038ff'] ); - /** sample vevent */ - $uid = Str::uuid().'@kithkin.lan'; - $start = Carbon::now(); - $end = Carbon::now()->addHour(); + /** + * + * create helper function for events to be added + **/ - $ical = <<copy()->addHour(); + + // create UTC copies for the ICS fields + $dtstamp = $start->copy()->utc()->format('Ymd\\THis\\Z'); + $dtstart = $start->copy()->utc()->format('Ymd\\THis\\Z'); + $dtend = $end->copy()->utc()->format('Ymd\\THis\\Z'); + + $ical = <<utc()->format('Ymd\\THis\\Z')} -DTSTART:{$start->format('Ymd\\THis')} -DTEND:{$end->format('Ymd\\THis')} -SUMMARY:Seed Event +DTSTAMP:$dtstamp +DTSTART:$dtstart +DTEND:$dtend +SUMMARY:$summary DESCRIPTION:Automatically seeded event LOCATION:Home Office END:VEVENT END:VCALENDAR ICS; - $eventId = DB::table('calendarobjects')->insertGetId([ - 'calendarid' => $calId, - 'uri' => Str::uuid().'.ics', - 'lastmodified' => time(), - 'etag' => md5($ical), - 'size' => strlen($ical), - 'componenttype' => 'VEVENT', - 'uid' => $uid, - 'calendardata' => $ical, - ]); + $eventId = DB::table('calendarobjects')->insertGetId([ + 'calendarid' => $calId, + 'uri' => Str::uuid().'.ics', + 'lastmodified' => time(), + 'etag' => md5($ical), + 'size' => strlen($ical), + 'componenttype' => 'VEVENT', + 'uid' => $uid, + 'calendardata' => $ical, + ]); - DB::table('event_meta')->updateOrInsert( - ['event_id' => $eventId], - [ - 'title' => 'Seed Event', - 'description' => 'Automatically seeded event', - 'location' => 'Home Office', - 'all_day' => false, - 'category' => 'Demo', - 'start_at' => $start, - 'end_at' => $end, - 'created_at' => now(), - 'updated_at' => now(), - ] - ); + DB::table('event_meta')->updateOrInsert( + ['event_id' => $eventId], + [ + 'title' => $summary, + 'description' => 'Automatically seeded event', + 'location' => 'Home Office', + 'all_day' => false, + 'category' => 'Demo', + 'start_at' => $start->copy()->utc(), + 'end_at' => $end->copy()->utc(), + 'created_at' => now(), + 'updated_at' => now(), + ] + ); + }; - /** create cards */ + /** + * + * create events + */ + + $now = Carbon::now()->setSeconds(0); + + // 3 events today + $insertEvent($now->copy(), 'Playground with James'); + $insertEvent($now->copy()->addHours(2), 'Lunch with Daniel'); + $insertEvent($now->copy()->addHours(4), 'Baseball practice'); + + // 1 event 3 days ago + $past = $now->copy()->subDays(3)->setTime(10, 0); + $insertEvent($past, 'Kids doctors appointments'); + + // 1 event 2 days ahead + $future2 = $now->copy()->addDays(2)->setTime(14, 0); + $insertEvent($future2, 'Teacher conference (Nuthatches)'); + + // 2 events 5 days ahead + $future5a = $now->copy()->addDays(5)->setTime(9, 0); + $future5b = $future5a->copy()->addHours(2); + $insertEvent($future5a, 'Teacher conference (3rd grade)'); + $insertEvent($future5b, 'Family game night'); + + /** + * + * address books + * + */ + + // create cards $bookId = DB::table('addressbooks')->insertGetId([ 'principaluri' => $user->uri, 'uri' => 'default', @@ -114,14 +168,14 @@ ICS; ]); $vcard = <<insert([ 'addressbook_id' => $bookId, diff --git a/package-lock.json b/package-lock.json index 4b69135..7173455 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,12 +4,14 @@ "requires": true, "packages": { "": { + "name": "kithkin", "devDependencies": { "@tailwindcss/forms": "^0.5.2", "@tailwindcss/postcss": "^4.1.11", "@tailwindcss/vite": "^4.1.11", "axios": "^1.8.2", "concurrently": "^9.0.1", + "htmx.org": "^2.0.6", "laravel-vite-plugin": "^1.2.0", "postcss": "^8.4.31", "tailwindcss": "^4.1.11", @@ -1526,9 +1528,9 @@ } }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, "license": "MIT", "dependencies": { @@ -1688,6 +1690,13 @@ "node": ">= 0.4" } }, + "node_modules/htmx.org": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.6.tgz", + "integrity": "sha512-7ythjYneGSk3yCHgtCnQeaoF+D+o7U2LF37WU3O0JYv3gTZSicdEFiI/Ai/NJyC5ZpYJWMpUb11OC5Lr6AfAqA==", + "dev": true, + "license": "0BSD" + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", diff --git a/package.json b/package.json index b8af6bb..388daaa 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@tailwindcss/vite": "^4.1.11", "axios": "^1.8.2", "concurrently": "^9.0.1", + "htmx.org": "^2.0.6", "laravel-vite-plugin": "^1.2.0", "postcss": "^8.4.31", "tailwindcss": "^4.1.11", diff --git a/resources/css/app.css b/resources/css/app.css index 066113d..e2e8423 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -12,6 +12,7 @@ @import './lib/indicator.css'; @import './lib/input.css'; @import './lib/mini.css'; +@import './lib/modal.css'; /** plugins */ @plugin '@tailwindcss/forms'; diff --git a/resources/css/etc/theme.css b/resources/css/etc/theme.css index 43e68bf..40b854e 100644 --- a/resources/css/etc/theme.css +++ b/resources/css/etc/theme.css @@ -60,7 +60,8 @@ --spacing-2px: 2px; --text-2xs: 0.625rem; - --text-2xs--line-height: 1.2; + --text-2xs--line-height: 1.3; + --text-xs : 0.8rem; --text-2xl: 1.75rem; --text-2xl--line-height: 1.333; --text-3xl: 2rem; diff --git a/resources/css/lib/calendar.css b/resources/css/lib/calendar.css index 8a181dd..951a739 100644 --- a/resources/css/lib/calendar.css +++ b/resources/css/lib/calendar.css @@ -16,7 +16,7 @@ grid-auto-rows: 1fr; li { - @apply relative px-1 pt-8 border-t-md border-primary; + @apply relative px-1 pt-8 border-t-md border-gray-900; &::before { @apply absolute top-0 right-px w-auto h-8 flex items-center justify-end pr-4 text-sm font-medium; @@ -48,16 +48,20 @@ } .title { - @apply grow; + @apply grow truncate; } time { - @apply text-2xs; + @apply text-2xs shrink-0 mt-px; } &:hover { background-color: color-mix(in srgb, var(--event-color) 25%, #fff 100%); } + + &.hidden { + @apply hidden; + } } } } diff --git a/resources/css/lib/modal.css b/resources/css/lib/modal.css new file mode 100644 index 0000000..7254dc3 --- /dev/null +++ b/resources/css/lib/modal.css @@ -0,0 +1,53 @@ +dialog { + @apply grid fixed top-0 right-0 bottom-0 left-0 m-0 p-0 pointer-events-none; + @apply justify-items-center items-start bg-transparent opacity-0 invisible; + @apply w-full h-full max-w-full max-h-full overflow-y-hidden; + background-color: rgba(26, 26, 26, 0.75); + backdrop-filter: blur(0.25rem); + grid-template-rows: minmax(20dvh, 2rem) 1fr; + transition: + opacity 150ms cubic-bezier(0,0,.2,1), + visibility 150ms cubic-bezier(0,0,.2,1); + z-index: 100; + + #modal { + @apply relative rounded-lg bg-white border-gray-200 p-0; + @apply flex flex-col items-start col-start-1 row-start-2 translate-y-4; + @apply overscroll-contain overflow-y-auto; + max-height: calc(100vh - 5em); + width: 91.666667%; + max-width: 36rem; + transition: all 150ms cubic-bezier(0,0,.2,1); + box-shadow: #00000040 0 1.5rem 4rem -0.5rem; + + > form { + @apply absolute top-4 right-4; + } + + > .content { + @apply w-full; + + /* modal header */ + h2 { + @apply pr-12; + } + } + } + + &::backdrop { + display: none; + } + + &[open] { + @apply opacity-100 visible; + pointer-events: inherit; + + #modal { + @apply translate-y-0; + } + + &::backdrop { + @apply opacity-100; + } + } +} diff --git a/resources/js/app.js b/resources/js/app.js index e59d6a0..a194545 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1 +1,26 @@ import './bootstrap'; +import htmx from 'htmx.org'; + +// make html globally visible to use the devtools and extensions +window.htmx = htmx; + +// global htmx config +htmx.config.historyEnabled = true; // HX-Boost back/forward support +htmx.logger = console.log; // verbose logging during dev + +// calendar toggle +// * progressive enhancement on html form with no js +document.addEventListener('change', event => { + const checkbox = event.target; + + // ignore anything that isn’t one of our checkboxes + if (!checkbox.matches('.calendar-toggle')) return; + + const slug = checkbox.value; + const show = checkbox.checked; + + // toggle .hidden on every matching event element + document + .querySelectorAll(`[data-calendar="${slug}"]`) + .forEach(el => el.classList.toggle('hidden', !show)); +}); diff --git a/resources/svg/icons/x.svg b/resources/svg/icons/x.svg new file mode 100644 index 0000000..eb194fd --- /dev/null +++ b/resources/svg/icons/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/views/book/create.blade.php b/resources/views/book/create.blade.php new file mode 100644 index 0000000..e293c25 --- /dev/null +++ b/resources/views/book/create.blade.php @@ -0,0 +1,4 @@ + +

Create Address Book

+ @include('addressbooks.form', ['action' => route('book.store'), 'isEdit' => false]) +
diff --git a/resources/views/book/edit.blade.php b/resources/views/book/edit.blade.php new file mode 100644 index 0000000..886ecb4 --- /dev/null +++ b/resources/views/book/edit.blade.php @@ -0,0 +1,4 @@ + +

Edit Address Book

+ @include('addressbooks.form', ['action' => route('book.update', $addressbook), 'isEdit' => true]) +
diff --git a/resources/views/books/form.blade.php b/resources/views/book/form.blade.php similarity index 100% rename from resources/views/books/form.blade.php rename to resources/views/book/form.blade.php diff --git a/resources/views/books/index.blade.php b/resources/views/book/index.blade.php similarity index 84% rename from resources/views/books/index.blade.php rename to resources/views/book/index.blade.php index 55370fd..97c70ec 100644 --- a/resources/views/books/index.blade.php +++ b/resources/views/book/index.blade.php @@ -1,7 +1,7 @@

- {{ __('My Address Books') }} + {{ __('Contacts') }}

@@ -13,7 +13,7 @@
    @forelse($books as $book)
  • - + {{ $book->displayname }}
  • diff --git a/resources/views/books/show.blade.php b/resources/views/book/show.blade.php similarity index 74% rename from resources/views/books/show.blade.php rename to resources/views/book/show.blade.php index f943e3b..b1e161e 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/book/show.blade.php @@ -10,6 +10,6 @@ @endforeach
- Edit Book - Back to all + Edit Book + Back to all
diff --git a/resources/views/books/create.blade.php b/resources/views/books/create.blade.php deleted file mode 100644 index 2a89040..0000000 --- a/resources/views/books/create.blade.php +++ /dev/null @@ -1,4 +0,0 @@ - -

Create Address Book

- @include('addressbooks.form', ['action' => route('addressbooks.store'), 'isEdit' => false]) -
\ No newline at end of file diff --git a/resources/views/books/edit.blade.php b/resources/views/books/edit.blade.php deleted file mode 100644 index 251ed6e..0000000 --- a/resources/views/books/edit.blade.php +++ /dev/null @@ -1,4 +0,0 @@ - -

Edit Address Book

- @include('addressbooks.form', ['action' => route('addressbooks.update', $addressbook), 'isEdit' => true]) -
\ No newline at end of file diff --git a/resources/views/calendar/index.blade.php b/resources/views/calendar/index.blade.php index cc85ca7..23e47a1 100644 --- a/resources/views/calendar/index.blade.php +++ b/resources/views/calendar/index.blade.php @@ -1,4 +1,5 @@ +

{{ __('Calendar') }} @@ -27,24 +28,39 @@ + + - + + + diff --git a/resources/views/components/calendar/day.blade.php b/resources/views/components/calendar/day.blade.php index e1f5df2..ce21787 100644 --- a/resources/views/components/calendar/day.blade.php +++ b/resources/views/components/calendar/day.blade.php @@ -1,5 +1,6 @@ @props([ 'day', // required + 'events', 'calendars' => [], // calendar palette keyed by id ]) @@ -13,14 +14,27 @@ 'day--outside' => !$day['in_month'], 'day--today' => $day['is_today'], ])> - @foreach ($day['events'] as $event) - @php - $bg = $calendars[(string) $event['calendar_id']]['color'] ?? '#999'; - @endphp - + @if(!empty($day['events'])) + @foreach (array_keys($day['events']) as $eventId) + @php + /* pull the full event once */ + $event = $events[$eventId]; + + /* calendar color */ + $bg = $calendars[$event['calendar_id']]['color'] ?? '#999'; + @endphp + {{ $event['title'] }} - @endforeach + @endforeach + @endif diff --git a/resources/views/components/calendar/full.blade.php b/resources/views/components/calendar/full.blade.php index be03f9b..6ab1da0 100644 --- a/resources/views/components/calendar/full.blade.php +++ b/resources/views/components/calendar/full.blade.php @@ -1,6 +1,7 @@ @props([ 'grid' => ['weeks' => []], 'calendars' => [], + 'events' => [], 'class' => '' ]) @@ -17,7 +18,7 @@
    @foreach ($grid['weeks'] as $week) @foreach ($week as $day) - + @endforeach @endforeach
diff --git a/resources/views/components/calendar/mini.blade.php b/resources/views/components/calendar/mini.blade.php index f13ed14..ec32c6e 100644 --- a/resources/views/components/calendar/mini.blade.php +++ b/resources/views/components/calendar/mini.blade.php @@ -7,13 +7,13 @@
- U - M - T - W - R - F - S + Mo + Tu + We + Th + Fr + Sa + Su
{{ $slot }} diff --git a/resources/views/components/modal.blade.php b/resources/views/components/modal.blade.php deleted file mode 100644 index 70704c1..0000000 --- a/resources/views/components/modal.blade.php +++ /dev/null @@ -1,78 +0,0 @@ -@props([ - 'name', - 'show' => false, - 'maxWidth' => '2xl' -]) - -@php -$maxWidth = [ - 'sm' => 'sm:max-w-sm', - 'md' => 'sm:max-w-md', - 'lg' => 'sm:max-w-lg', - 'xl' => 'sm:max-w-xl', - '2xl' => 'sm:max-w-2xl', -][$maxWidth]; -@endphp - -
-
-
-
- -
- {{ $slot }} -
-
diff --git a/resources/views/components/modal/content.blade.php b/resources/views/components/modal/content.blade.php new file mode 100644 index 0000000..27e1bd4 --- /dev/null +++ b/resources/views/components/modal/content.blade.php @@ -0,0 +1,8 @@ + + + + +
+
+ {{ $slot }} +
diff --git a/resources/views/components/modal/index.blade.php b/resources/views/components/modal/index.blade.php new file mode 100644 index 0000000..d7d97ee --- /dev/null +++ b/resources/views/components/modal/index.blade.php @@ -0,0 +1,8 @@ + + + + diff --git a/resources/views/events/form.blade.php b/resources/views/event/form.blade.php similarity index 100% rename from resources/views/events/form.blade.php rename to resources/views/event/form.blade.php diff --git a/resources/views/event/partials/details.blade.php b/resources/views/event/partials/details.blade.php new file mode 100644 index 0000000..3b7a74c --- /dev/null +++ b/resources/views/event/partials/details.blade.php @@ -0,0 +1,27 @@ + +
+

+ {{ $event->meta->title ?? '(no title)' }} +

+ +

+ {{ $start->format('l, F j, Y · g:i A') }} + @unless ($start->equalTo($end)) +  –  + {{ $end->isSameDay($start) + ? $end->format('g:i A') + : $end->format('l, F j, Y · g:i A') }} + @endunless +

+ + @if ($event->meta->location) +

Where: {{ $event->meta->location }}

+ @endif + + @if ($event->meta->description) +
+ {!! nl2br(e($event->meta->description)) !!} +
+ @endif +
+
diff --git a/resources/views/event/show.blade.php b/resources/views/event/show.blade.php new file mode 100644 index 0000000..77517ab --- /dev/null +++ b/resources/views/event/show.blade.php @@ -0,0 +1,14 @@ + + +

+ {{ $event->meta->title ?? '(no title)' }} +

+ + + Edit + +
+ + @include('event.partials.details') +
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 967f89e..3f99a65 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -8,8 +8,11 @@ @vite(['resources/css/app.css', 'resources/js/app.js']) + + @include('layouts.navigation') +
@isset($header)
@@ -22,6 +25,7 @@
+ + + + + diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index 79d0fe1..8b7723b 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -14,7 +14,7 @@ - + diff --git a/resources/views/subscription/create.blade.php b/resources/views/subscription/create.blade.php new file mode 100644 index 0000000..831d311 --- /dev/null +++ b/resources/views/subscription/create.blade.php @@ -0,0 +1,9 @@ + + +

Add Remote Subscription

+
+ + +
diff --git a/resources/views/subscription/edit.blade.php b/resources/views/subscription/edit.blade.php new file mode 100644 index 0000000..f0a67d6 --- /dev/null +++ b/resources/views/subscription/edit.blade.php @@ -0,0 +1,10 @@ + + +

Edit Subscription

+
+ + +
diff --git a/resources/views/subscription/form.blade.php b/resources/views/subscription/form.blade.php new file mode 100644 index 0000000..1fcdf65 --- /dev/null +++ b/resources/views/subscription/form.blade.php @@ -0,0 +1,44 @@ +@props(['action', 'method' => 'POST', 'subscription' => null]) + +
+ @csrf + @if($method !== 'POST') + @method($method) + @endif + +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
diff --git a/resources/views/subscription/index.blade.php b/resources/views/subscription/index.blade.php new file mode 100644 index 0000000..726cd5d --- /dev/null +++ b/resources/views/subscription/index.blade.php @@ -0,0 +1,47 @@ + + +

Remote Subscriptions

+ + New +
+ + + + + + + + + + + + @forelse ($subs as $sub) + + + + + + + @empty + + @endforelse + +
NameSource URLColour
{{ $sub->displayname ?: '(no label)' }}{{ $sub->source }} + + + edit + +
+ @csrf @method('DELETE') + +
+
+ No subscriptions yet +
+
diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php index 57e5476..7b2c84c 100644 --- a/resources/views/welcome.blade.php +++ b/resources/views/welcome.blade.php @@ -1,277 +1,6 @@ - - - - - - - Laravel - - - - - - - @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) - @vite(['resources/css/app.css', 'resources/js/app.js']) - @else - - @endif - - -
- @if (Route::has('login')) - - @endif -
-
-
-
-

Let's get started

-

Laravel has an incredibly rich ecosystem.
We suggest starting with the following.

- - -
-
- {{-- Laravel Logo --}} - - - - - - - - - - - {{-- Light Mode 12 SVG --}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{-- Dark Mode 12 SVG --}} - -
-
-
-
- - @if (Route::has('login')) - - @endif - - + +

Welcome to Kithkin Dev Mode!

+ + Calendar + +
diff --git a/routes/web.php b/routes/web.php index 0e04480..9fa04fb 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,6 +7,7 @@ use App\Http\Controllers\CalendarController; use App\Http\Controllers\CardController; use App\Http\Controllers\DavController; use App\Http\Controllers\EventController; +use App\Http\Controllers\IcsController; use App\Http\Controllers\SubscriptionController; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; @@ -38,31 +39,48 @@ Route::middleware('auth')->group(function () { Route::patch ('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); - /* Calendars CRUD */ - Route::resource('calendar', CalendarController::class); + /* calendar core */ + Route::middleware('auth')->group(function () { + // list, create, store, show, edit, update, destroy + Route::resource('calendar', CalendarController::class) + ->whereUuid('calendar'); // constrain the {calendar} param + }); - /* Nested Events CRUD */ - Route::prefix('calendar/{calendar}') + /* calendar other */ + Route::middleware('auth') + ->prefix('calendar') ->name('calendar.') ->group(function () { - Route::get ('events/create', [EventController::class, 'create'])->name('events.create'); - Route::post('events', [EventController::class, 'store' ])->name('events.store'); - Route::get ('events/{event}/edit', [EventController::class, 'edit' ])->name('events.edit'); - Route::put ('events/{event}', [EventController::class, 'update'])->name('events.update'); + // remote calendar subscriptions + Route::resource('subscriptions', SubscriptionController::class) + ->except(['show']); // index, create, store, edit, update, destroy + // events + Route::prefix('{calendar}')->whereUuid('calendar')->group(function () { + // create & store + Route::get ('event/create', [EventController::class, 'create'])->name('events.create'); + Route::post('event', [EventController::class, 'store' ])->name('events.store'); + // read + Route::get ('event/{event}', [EventController::class, 'show' ])->name('events.show'); + // edit & update + Route::get ('event/{event}/edit', [EventController::class, 'edit' ])->name('events.edit'); + Route::put ('event/{event}', [EventController::class, 'update'])->name('events.update'); + // delete + Route::delete('event/{event}', [EventController::class, 'destroy'])->name('events.destroy'); + }); }); /** address books */ - Route::resource('books', BookController::class) - ->names('books') // books.index, books.create, … - ->parameter('books', 'book'); // {book} binding + Route::resource('book', BookController::class) + ->names('book') // books.index, books.create, … + ->parameter('book', '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') + /*Route::resource('book.contacts', CardController::class) + ->names('book.contacts') ->parameter('contacts', 'contact') ->except(['index']) // you may add an index later - ->shallow(); + ->shallow();*/ }); /* Breeze auth routes (login, register, password reset, etc.) */ @@ -84,5 +102,11 @@ Route::match( )->where('any', '.*') ->withoutMiddleware([VerifyCsrfToken::class]); -// subscriptions -Route::get('/subscriptions/{calendarUri}.ics', [SubscriptionController::class, 'download']); +// our subscriptions +Route::get('/ics/{calendarUri}.ics', [IcsController::class, 'download']); + +// remote subscriptions +Route::middleware(['auth'])->group(function () { + Route::resource('calendar/subscriptions', SubscriptionController::class) + ->except(['show']); // index • create • store • edit • update • destroy +});