From 98fb10bc14937af5d9d6d42774d1b8285a2011cf Mon Sep 17 00:00:00 2001 From: Andrew Gioia Date: Sat, 2 Aug 2025 06:52:26 -0400 Subject: [PATCH] Adds calendar settings page and handling, adds new calendar subscription functionality, fixes big calendar object for display, improvements to object --- app/Http/Controllers/CalendarController.php | 130 +++++++++++++++--- .../CalendarSettingsController.php | 62 +++++++++ .../Controllers/SubscriptionController.php | 32 +++-- app/Http/Middleware/HtmxAwareAuthenticate.php | 29 ++++ app/Models/Subscription.php | 54 +++++++- composer.lock | 10 +- ...7_15_000001_create_calendar_meta_table.php | 25 ++-- resources/css/etc/layout.css | 75 +++++++++- resources/css/etc/theme.css | 4 +- resources/css/etc/type.css | 13 ++ resources/css/lib/button.css | 18 ++- resources/css/lib/checkbox.css | 4 + resources/css/lib/input.css | 50 ++++++- resources/css/lib/mini.css | 10 +- resources/svg/icons/calendar-sync.svg | 1 + resources/svg/icons/globe.svg | 1 + resources/views/calendar/index.blade.php | 72 ++++++---- .../views/calendar/settings/index.blade.php | 30 ++++ .../calendar/settings/subscribe.blade.php | 33 +++++ .../nav-button.blade.php} | 0 .../views/components/app/pagelink.blade.php | 11 ++ .../views/components/calendar/day.blade.php | 7 +- .../views/components/calendar/mini.blade.php | 60 ++++++-- .../calendar/settings-menu.blade.php | 28 ++++ .../components/input/checkbox-label.blade.php | 14 ++ .../views/components/input/checkbox.blade.php | 14 ++ .../components/input/text-label.blade.php | 26 ++++ .../views/components/input/text.blade.php | 17 +++ resources/views/layouts/guest.blade.php | 2 +- resources/views/layouts/navigation.blade.php | 12 +- routes/web.php | 9 ++ 31 files changed, 736 insertions(+), 117 deletions(-) create mode 100644 app/Http/Controllers/CalendarSettingsController.php create mode 100644 app/Http/Middleware/HtmxAwareAuthenticate.php create mode 100644 resources/svg/icons/calendar-sync.svg create mode 100644 resources/svg/icons/globe.svg create mode 100644 resources/views/calendar/settings/index.blade.php create mode 100644 resources/views/calendar/settings/subscribe.blade.php rename resources/views/components/{nav-link.blade.php => app/nav-button.blade.php} (100%) create mode 100644 resources/views/components/app/pagelink.blade.php create mode 100644 resources/views/components/calendar/settings-menu.blade.php create mode 100644 resources/views/components/input/checkbox-label.blade.php create mode 100644 resources/views/components/input/checkbox.blade.php create mode 100644 resources/views/components/input/text-label.blade.php create mode 100644 resources/views/components/input/text.blade.php diff --git a/app/Http/Controllers/CalendarController.php b/app/Http/Controllers/CalendarController.php index 62898d1..0063a59 100644 --- a/app/Http/Controllers/CalendarController.php +++ b/app/Http/Controllers/CalendarController.php @@ -12,6 +12,7 @@ use App\Models\CalendarMeta; use App\Models\CalendarInstance; use App\Models\Event; use App\Models\EventMeta; +use App\Models\Subscription; class CalendarController extends Controller { @@ -44,26 +45,47 @@ class CalendarController extends Controller // get the user's visible calendars from the left bar $visible = collect($request->query('c', [])); - // load the user's calendars - $calendars = Calendar::query() + // load the user's local calendars + $locals = 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' + 'ci.uri as slug', + 'ci.timezone as timezone', + 'meta.color as meta_color', + 'meta.color_fg as meta_color_fg', + DB::raw('false as is_remote') ) ->join('calendarinstances as ci', 'ci.calendarid', '=', 'calendars.id') ->leftJoin('calendar_meta as meta', 'meta.calendar_id', '=', 'calendars.id') ->where('ci.principaluri', $principal) ->orderBy('ci.displayname') - ->get() - ->map(function ($cal) use ($visible) { - $cal->visible = $visible->isEmpty() || $visible->contains($cal->slug); - return $cal; - }); + ->get(); + + // load the users remote/subscription calendars + $remotes = Subscription::query() + ->join('calendar_meta as m', 'm.subscription_id', '=', 'calendarsubscriptions.id') + ->where('principaluri', $principal) + ->orderBy('displayname') + ->select( + 'calendarsubscriptions.id', + 'calendarsubscriptions.displayname', + 'calendarsubscriptions.calendarcolor', + 'calendarsubscriptions.uri as slug', + DB::raw('NULL as timezone'), + 'm.color as meta_color', + 'm.color_fg as meta_color_fg', + DB::raw('true as is_remote') + ) + ->get(); + + // merge local and remote, and add the visibility flag + $visible = collect($request->query('c', [])); + $calendars = $locals->merge($remotes)->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'); @@ -101,9 +123,22 @@ class CalendarController extends Controller 'end_ui' => optional($end_local)->format('g:ia'), 'timezone' => $timezone, 'visible' => $cal->visible, + 'color' => $cal->meta_color ?? $cal->calendarcolor ?? '#1a1a1a', + 'color_fg' => $cal->meta_color_fg ?? '#ffffff', ]; })->keyBy('id'); + // create the mini calendar grid based on the mini cal controls + $mini_anchor = $request->query('mini', $range['start']->toDateString()); + $mini_start = Carbon::parse($mini_anchor)->startOfMonth(); + $mini_nav = [ + 'prev' => $mini_start->copy()->subMonth()->toDateString(), + 'next' => $mini_start->copy()->addMonth()->toDateString(), + 'today' => Carbon::today()->startOfMonth()->toDateString(), + 'label' => $mini_start->format('F Y'), + ]; + $mini = $this->buildMiniGrid($mini_start, $events); + // create the calendar grid of days $grid = $this->buildCalendarGrid($view, $range, $events); @@ -121,18 +156,23 @@ class CalendarController extends Controller 'month' => $range['start']->format("F"), 'day' => $range['start']->format("d"), ], - 'calendars' => $calendar_map->map(function ($cal) { + 'calendars' => $calendars->mapWithKeys(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 - 'visible' => true, // default to visible; the UI can toggle this + $cal->id => [ + 'id' => $cal->id, + 'slug' => $cal->slug, + 'name' => $cal->displayname, + 'color' => $cal->meta_color ?? $cal->calendarcolor ?? '#1a1a1a', + 'color_fg' => $cal->meta_color_fg ?? '#ffffff', + 'visible' => $cal->visible, + 'is_remote' => $cal->is_remote, + ], ]; }), - 'events' => $events, // keyed, one copy each - 'grid' => $grid, // day objects hold only ID-sets + 'events' => $events, // keyed, one copy each + 'grid' => $grid, // day objects hold only ID-sets + 'mini' => $mini, // mini calendar days with events for indicators + 'mini_nav' => $mini_nav, // separate mini calendar navigation ]; return view('calendar.index', $payload); @@ -386,4 +426,54 @@ class CalendarController extends Controller ? ['days' => $days, 'weeks' => array_chunk($days, 7)] : ['days' => $days]; } + + /** + * Build the mini-month grid for day buttons + * + * Returns ['days' => [ + * [ + * 'date' => '2025-06-30', + * 'label' => '30', + * 'in_month' => false, + * 'events' => [id, id …] + * ], … + * ]] + */ + private function buildMiniGrid(Carbon $monthStart, Collection $events): array + { + // get bounds + $monthEnd = $monthStart->copy()->endOfMonth(); + $gridStart = $monthStart->copy()->startOfWeek(Carbon::MONDAY); + $gridEnd = $monthEnd->copy()->endOfWeek(Carbon::SUNDAY); + + // ensure we have 42 days (6 rows); 35 = add one extra week + if ($gridStart->diffInDays($gridEnd) + 1 < 42) { + $gridEnd->addWeek(); + } + + /* map event-ids by yyyy-mm-dd */ + $byDay = []; + foreach ($events as $ev) { + $s = Carbon::parse($ev['start']); + $e = $ev['end'] ? Carbon::parse($ev['end']) : $s; + for ($d = $s->copy()->startOfDay(); $d->lte($e); $d->addDay()) { + $byDay[$d->toDateString()][] = $ev['id']; + } + } + + /* Walk the 42-day span */ + $days = []; + for ($d = $gridStart->copy(); $d->lte($gridEnd); $d->addDay()) { + $iso = $d->toDateString(); + $days[] = [ + 'date' => $iso, + 'label' => $d->format('j'), + 'in_month' => $d->between($monthStart, $monthEnd), + 'events' => $byDay[$iso] ?? [], + ]; + } + + // will always be 42 to ensure 6 rows + return ['days' => $days]; + } } diff --git a/app/Http/Controllers/CalendarSettingsController.php b/app/Http/Controllers/CalendarSettingsController.php new file mode 100644 index 0000000..0599499 --- /dev/null +++ b/app/Http/Controllers/CalendarSettingsController.php @@ -0,0 +1,62 @@ +frame('calendar.settings.subscribe'); + } + + /* show “Subscribe to a calendar” form */ + public function subscribeForm() + { + return $this->frame( + 'calendar.settings.subscribe', + [ + 'title' => 'Subscribe to a calendar', + 'sub' => 'Add an `.ics` calender from another service' + ]); + } + + /* handle POST from the subscribe form */ + public function subscribeStore(Request $request) + { + $data = $request->validate([ + 'source' => ['required', 'url'], + 'displayname' => ['nullable', 'string', 'max:100'], + 'color' => ['nullable', 'regex:/^#[0-9A-F]{6}$/i'], + ]); + + DB::table('calendarsubscriptions')->insert([ + 'uri' => Str::uuid(), // local id + 'principaluri' => 'principals/'.$request->user()->email, + 'source' => $data['source'], + 'displayname' => $data['displayname'] ?: $data['source'], + 'calendarcolor' => $data['color'], + 'refreshrate' => 'P1D', // daily + 'lastmodified' => now()->timestamp, + ]); + + return redirect() + ->route('calendar.settings') + ->with('toast', __('Subscription added successfully!')); + } + + /** + * content frame handler + */ + private function frame(?string $view = null, array $data = []) + { + return view('calendar.settings.index', [ + 'view' => $view, + 'data' => $data, + ]); + } +} diff --git a/app/Http/Controllers/SubscriptionController.php b/app/Http/Controllers/SubscriptionController.php index 0884d76..f21e5f7 100644 --- a/app/Http/Controllers/SubscriptionController.php +++ b/app/Http/Controllers/SubscriptionController.php @@ -32,21 +32,11 @@ class SubscriptionController extends Controller '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, - ]); + Subscription::createWithMeta($request->user(), $data); - return redirect()->route('subscription.index') - ->with('toast', __('Subscription added!')); + return redirect() + ->route('calendar.index') + ->with('toast', __('Subscription added!')); } public function edit(Subscription $subscription) @@ -68,8 +58,22 @@ class SubscriptionController extends Controller 'stripattachments' => 'sometimes|boolean', ]); + // update calendarsubscriptions record $subscription->update($data); + // update corresponding calendar_meta record + $subscription->meta()->updateOrCreate( + [], // no “where” clause → look at subscription_id FK + [ + 'title' => $subscription->displayname, + 'color' => $subscription->calendarcolor ?? '#1a1a1a', + 'color_fg' => contrast_text_color( + $subscription->calendarcolor ?? '#1a1a1a' + ), + 'updated_at'=> now(), + ] + ); + return back()->with('toast', __('Subscription updated!')); } diff --git a/app/Http/Middleware/HtmxAwareAuthenticate.php b/app/Http/Middleware/HtmxAwareAuthenticate.php new file mode 100644 index 0000000..0de10a8 --- /dev/null +++ b/app/Http/Middleware/HtmxAwareAuthenticate.php @@ -0,0 +1,29 @@ +isMethod('get') ? route('login') : null; + } + + protected function unauthenticated($request, array $guards) + { + // If it was an HTMX request, send 401 + HX-Redirect instead of 302 + if ($request->header('HX-Request')) { + abort( + response('') + ->header('HX-Redirect', route('login')) + ->setStatusCode(401) + ); + } + + parent::unauthenticated($request, $guards); + } +} diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index e0c46f9..b75aa6f 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -3,11 +3,15 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; +use App\Models\User; class Subscription extends Model { - protected $table = 'calendarsubscriptions'; - public $timestamps = false; // sabre table without created_at/updated_at; may need a meta table? + /** basic table mapping */ + protected $table = 'calendarsubscriptions'; + public $timestamps = false; // sabre table protected $fillable = [ 'uri', @@ -22,10 +26,54 @@ class Subscription extends Model 'stripattachments', ]; - /** Cast tinyint columns to booleans */ protected $casts = [ 'striptodos' => 'bool', 'stripalarms' => 'bool', 'stripattachments' => 'bool', ]; + + /** relationship to meta row */ + public function meta() + { + return $this->hasOne(\App\Models\CalendarMeta::class, + 'subscription_id'); + } + + /** + * Create a remote calendar subscription and its UI metadata (calendar_meta). + */ + public static function createWithMeta(User $user, array $data): self + { + return DB::transaction(function () use ($user, $data) { + + /** insert into calendarsubscriptions */ + $sub = self::create([ + 'uri' => (string) Str::uuid(), + 'principaluri' => $user->principal_uri, + 'source' => $data['source'], + 'displayname' => $data['displayname'] ?? $data['source'], + 'calendarcolor' => $data['calendarcolor'] ?? null, + 'refreshrate' => $data['refreshrate'] ?? 'P1D', + 'calendarorder' => 0, + 'striptodos' => false, + 'stripalarms' => false, + 'stripattachments' => false, + 'lastmodified' => now()->timestamp, + ]); + + /** create corresponding calendar_meta row */ + DB::table('calendar_meta')->insert([ + 'subscription_id' => $sub->id, + 'is_remote' => true, + 'title' => $sub->displayname, + 'color' => $sub->calendarcolor ?? '#1a1a1a', + 'color_fg' => contrast_text_color($sub->calendarcolor ?? '#1a1a1a'), + 'is_shared' => true, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + return $sub; + }); + } } diff --git a/composer.lock b/composer.lock index c8d97f7..b6fcbb8 100644 --- a/composer.lock +++ b/composer.lock @@ -1137,16 +1137,16 @@ }, { "name": "laravel/framework", - "version": "v12.20.0", + "version": "v12.21.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "1b9a00f8caf5503c92aa436279172beae1a484ff" + "reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/1b9a00f8caf5503c92aa436279172beae1a484ff", - "reference": "1b9a00f8caf5503c92aa436279172beae1a484ff", + "url": "https://api.github.com/repos/laravel/framework/zipball/ac8c4e73bf1b5387b709f7736d41427e6af1c93b", + "reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b", "shasum": "" }, "require": { @@ -1348,7 +1348,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-07-08T15:02:21+00:00" + "time": "2025-07-22T15:41:55+00:00" }, { "name": "laravel/prompts", 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 74977cf..9a6f6fb 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 @@ -10,20 +10,27 @@ return new class extends Migration { Schema::create('calendar_meta', function (Blueprint $table) { - // // FK = PK to Sabre’s calendars.id - $table->unsignedInteger('calendar_id')->primary(); // FK = PK + /** keys */ + $table->id(); // primary key + $table->unsignedInteger('calendar_id')->nullable()->unique(); + $table->unsignedInteger('subscription_id')->nullable()->unique(); - // UI fields - $table->string('title')->nullable(); // ui override - $table->string('color', 7)->nullable(); // bg color - $table->string('color_fg', 7)->nullable(); // fg color + /** ui-specific fields */ + $table->string('title')->nullable(); // override name + $table->string('color', 7)->nullable(); // background + $table->string('color_fg', 7)->nullable(); // foreground $table->boolean('is_shared')->default(false); - $table->json('settings')->nullable(); // arbitrary JSON + $table->boolean('is_remote')->default(false); // local vs. subscription + $table->json('settings')->nullable(); // arbitrary JSON $table->timestamps(); + /** foreign-key constraints */ $table->foreign('calendar_id') - ->references('id') - ->on('calendars') + ->references('id')->on('calendars') + ->cascadeOnDelete(); + + $table->foreign('subscription_id') + ->references('id')->on('calendarsubscriptions') ->cascadeOnDelete(); }); } diff --git a/resources/css/etc/layout.css b/resources/css/etc/layout.css index ce9108e..52ffa13 100644 --- a/resources/css/etc/layout.css +++ b/resources/css/etc/layout.css @@ -84,7 +84,8 @@ body { * section */ main { - @apply rounded-lg bg-white; + @apply overflow-hidden rounded-lg; + max-height: calc(100dvh - 1rem); /* app */ body#app & { @@ -98,37 +99,76 @@ main { /* auth screens */ body#auth & { - @apply w-1/2 mx-auto p-8; + @apply bg-white border-md border-primary shadow-drop w-1/2 mx-auto p-8 rounded-xl; min-width: 16rem; max-width: 40rem; } /* left column */ aside { - @apply flex flex-col col-span-1 px-6 2xl:px-8 pb-8 h-full; + @apply bg-white flex flex-col col-span-1 pb-8 h-full rounded-l-lg; + @apply overflow-y-auto; > h1 { - @apply flex items-center h-20; + @apply flex items-center h-20 min-h-20 px-6 2xl:px-8; + @apply backdrop-blur-xs sticky top-0 z-1 shrink-0; + background-color: rgba(255, 255, 255, 0.9); + } + + > .aside-inset { + @apply px-6 2xl:px-8; + } + + .content { + @apply grow; + } + + .drawers { + @apply grow flex flex-col gap-6 pt-1; + } + + .pagelinks { + @apply flex flex-col gap-2px -mx-3; + width: calc(100% + 1.5rem); + + a { + @apply flex flex-row gap-2 items-center justify-start px-3 h-9 rounded-md; + transition: background-color 100ms ease-in-out; + + &:hover { + @apply bg-cyan-200; + } + + &.is-active { + @apply bg-cyan-400; + + &:hover { + @apply bg-cyan-500; + } + } + } } } /* main content wrapper */ article { - @apply grid grid-cols-1 w-full pr-6 2xl:pr-8; + @apply bg-white grid grid-cols-1 w-full pl-3 2xl:pl-4 pr-6 2xl:pr-8 rounded-r-lg; + @apply overflow-y-auto; grid-template-rows: 5rem auto; /* main content title and actions */ > header { @apply flex flex-row items-center justify-between w-full; + @apply bg-white sticky top-0; /* if h1 exists it means there's no aside, so force the width from that */ h1 { - @apply flex items-center pl-6 2xl:pl-8; + @apply relative flex items-center pl-6 2xl:pl-8; width: minmax(20rem, 20dvw); } h2 { - @apply flex flex-row gap-1 items-center justify-start relative top-px; + @apply flex flex-row gap-1 items-center justify-start relative top-2px; > span { @apply text-gray-700; @@ -139,6 +179,27 @@ main { @apply flex flex-row items-center justify-end gap-4; } } + + /* sections with max width content */ + &.readable { + grid-template-columns: repeat(4, 1fr); + + > header { + @apply col-span-4; + } + + > .content { + @apply col-span-4 2xl:col-span-3; + } + } + + /* section specific */ + &#calendar { + /* */ + } + &#settings { + /* */ + } } } @media (width >= 96rem) { /* 2xl */ diff --git a/resources/css/etc/theme.css b/resources/css/etc/theme.css index 40b854e..e1c38af 100644 --- a/resources/css/etc/theme.css +++ b/resources/css/etc/theme.css @@ -61,7 +61,9 @@ --text-2xs: 0.625rem; --text-2xs--line-height: 1.3; - --text-xs : 0.8rem; + --text-xs: 0.8rem; + --text-md: 1.075rem; + --text-md--line-height: 1.4; --text-2xl: 1.75rem; --text-2xl--line-height: 1.333; --text-3xl: 2rem; diff --git a/resources/css/etc/type.css b/resources/css/etc/type.css index d5fa61a..02ccb2f 100644 --- a/resources/css/etc/type.css +++ b/resources/css/etc/type.css @@ -46,3 +46,16 @@ a { } } } + +/* text */ +p { + @apply text-base leading-normal; + + &.big { + @apply text-lg font-medium; + } +} + +.description { /* contains

and uses gap for spacing */ + @apply space-y-3; +} diff --git a/resources/css/lib/button.css b/resources/css/lib/button.css index 4ff4908..d796fee 100644 --- a/resources/css/lib/button.css +++ b/resources/css/lib/button.css @@ -6,12 +6,12 @@ button, --button-accent: var(--color-primary-hover); &.button--primary { - @apply bg-cyan-300 border-md border-solid; + @apply bg-cyan-400 border-md border-solid; border-color: var(--button-border); box-shadow: 2.5px 2.5px 0 0 var(--button-border); &:hover { - @apply bg-cyan-400; + @apply bg-cyan-500; border-color: var(--button-accent); } @@ -30,6 +30,10 @@ button, background-color: rgba(0,0,0,0.075); } } + + &.button--sm { + @apply text-base px-2 h-8; + } } /* button groups are used with labels/checks as well as regular buttons */ @@ -49,16 +53,20 @@ button, &:has(input:checked), &:active { - @apply bg-cyan-300 border-r-transparent; + @apply bg-cyan-400 border-r-transparent; box-shadow: inset 2.5px 0 0 0 var(--color-primary), - inset 0 0.25rem 0 0 var(--color-cyan-400); + inset 0 0.25rem 0 0 var(--color-cyan-500); top: 2.5px; + label, + button { box-shadow: inset 1.5px 0 0 0 var(--color-primary); } + + &:hover { + @apply bg-cyan-500; + } } &:first-child { @@ -66,7 +74,7 @@ button, &:has(input:checked), &:active { - box-shadow: inset 0 0.25rem 0 0 var(--color-cyan-400); + box-shadow: inset 0 0.25rem 0 0 var(--color-cyan-500); } } diff --git a/resources/css/lib/checkbox.css b/resources/css/lib/checkbox.css index d5159cc..c74a3d4 100644 --- a/resources/css/lib/checkbox.css +++ b/resources/css/lib/checkbox.css @@ -14,3 +14,7 @@ input[type="checkbox"] { box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--checkbox-color), var(--tw-shadow); } } + +label.checkbox-label { + @apply flex flex-row items-center gap-2; +} diff --git a/resources/css/lib/input.css b/resources/css/lib/input.css index e9bb534..8978abe 100644 --- a/resources/css/lib/input.css +++ b/resources/css/lib/input.css @@ -1,9 +1,57 @@ +/** + * default text inputs + */ input[type="email"], -input[type="text"], input[type="password"], +input[type="text"], +input[type="url"], input[type="search"] { @apply border-md border-gray-800 bg-white rounded-md shadow-input; @apply focus:border-primary focus:ring-2 focus:ring-offset-2 focus:ring-cyan-600; transition: box-shadow 125ms ease-in-out, border-color 125ms ease-in-out; } + +/** + * specific minor types + */ +input[type="color"] { + @apply rounded-md border-white shadow-none h-10 w-16 cursor-pointer; + @apply self-start; + + &::-moz-color-swatch { + @apply border-none; + } +} + +/** + * labeled text inputs + */ +label.text-label { + @apply flex flex-col gap-2; + + > .label { + @apply font-semibold text-md; + } + + > .description { + @apply text-gray-800; + } +} + +/** + * form layouts + */ +.form-grid-1 { + @apply grid grid-cols-3 gap-6; + + > label { + @apply col-span-2 col-start-1; + } + + > div { + @apply col-span-3 flex flex-row justify-start gap-4 pt-4; + } +} + + diff --git a/resources/css/lib/mini.css b/resources/css/lib/mini.css index ecb6cdb..7396993 100644 --- a/resources/css/lib/mini.css +++ b/resources/css/lib/mini.css @@ -3,15 +3,19 @@ /* mini controls */ header{ - @apply flex items-center justify-between px-2 pb-3; + @apply flex flex-row items-center justify-between px-2 pb-2; > span { - @apply font-serif text-lg; + @apply font-serif text-lg grow; + } + + > form { + @apply flex flex-row items-center justify-end gap-1 shrink-0 text-sm; } } /* days wrapper */ - figure { + nav { @apply border-md border-primary shadow-drop rounded-md; /* weekdays */ diff --git a/resources/svg/icons/calendar-sync.svg b/resources/svg/icons/calendar-sync.svg new file mode 100644 index 0000000..07e2af8 --- /dev/null +++ b/resources/svg/icons/calendar-sync.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/svg/icons/globe.svg b/resources/svg/icons/globe.svg new file mode 100644 index 0000000..06962a8 --- /dev/null +++ b/resources/svg/icons/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/views/calendar/index.blade.php b/resources/views/calendar/index.blade.php index 6b97c77..8f84f09 100644 --- a/resources/views/calendar/index.blade.php +++ b/resources/views/calendar/index.blade.php @@ -4,25 +4,24 @@

{{ __('Calendar') }}

-
-
- {{ __('My Calendars') }} -
+ -
    - @foreach ($calendars as $cal) +
    + {{ __('My Calendars') }} +
      + @foreach ($calendars->where('is_remote', false) as $cal)
    • - +
    • @endforeach
    @@ -31,16 +30,31 @@ - -
    +
+
+ + {{ __('Other Calendars') }} + + + +
    + @foreach ($calendars->where('is_remote', true) as $cal) +
  • + +
  • + @endforeach +
+
+
- - @foreach ($grid['weeks'] as $week) - @foreach ($week as $day) - - @endforeach - @endforeach - + @@ -63,13 +77,13 @@ {{-- keep current view (month/week/4day) --}} @@ -91,7 +105,7 @@
  • - +
  • diff --git a/resources/views/calendar/settings/index.blade.php b/resources/views/calendar/settings/index.blade.php new file mode 100644 index 0000000..b464dda --- /dev/null +++ b/resources/views/calendar/settings/index.blade.php @@ -0,0 +1,30 @@ + + + +

    + {{ __('Settings') }} +

    + +
    + + +

    + @isset($data['title']) + {{ $data['title'] }} + @else + {{ __('Settings') }} + @endisset +

    +
    + + +
    + @isset($view) + @include($view, $data ?? []) + @else +

    {{ __('Pick an option in the sidebar…') }}

    + @endisset +
    + + + diff --git a/resources/views/calendar/settings/subscribe.blade.php b/resources/views/calendar/settings/subscribe.blade.php new file mode 100644 index 0000000..1406d14 --- /dev/null +++ b/resources/views/calendar/settings/subscribe.blade.php @@ -0,0 +1,33 @@ +
    +

    + Subscribing to a calendar adds it to your Kithkin calendar set and syncs any changes back to the original service. This means that any changes you make here will be sent to that server, and vice versa. +

    + It's possible (and likely) that some of the things you can add to your calendar here will not work in other calendar apps or services. You'll always see that data here, though. +

    +
    +
    + @csrf + + + + + +
    + {{ __('Subscribe') }} + {{ __('Cancel and go back') }} +
    + diff --git a/resources/views/components/nav-link.blade.php b/resources/views/components/app/nav-button.blade.php similarity index 100% rename from resources/views/components/nav-link.blade.php rename to resources/views/components/app/nav-button.blade.php diff --git a/resources/views/components/app/pagelink.blade.php b/resources/views/components/app/pagelink.blade.php new file mode 100644 index 0000000..344e981 --- /dev/null +++ b/resources/views/components/app/pagelink.blade.php @@ -0,0 +1,11 @@ +@props(['active']) + +@php +$classes = ($active ?? false) + ? 'is-active' + : ''; +@endphp + +merge(['class' => $classes]) }}> + {{ $slot }} + diff --git a/resources/views/components/calendar/day.blade.php b/resources/views/components/calendar/day.blade.php index ce21787..2bd9df2 100644 --- a/resources/views/components/calendar/day.blade.php +++ b/resources/views/components/calendar/day.blade.php @@ -17,11 +17,8 @@ @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'; + $color = $event['color'] ?? '#999'; @endphp {{ $event['title'] }} diff --git a/resources/views/components/calendar/mini.blade.php b/resources/views/components/calendar/mini.blade.php index ec32c6e..f58e6bd 100644 --- a/resources/views/components/calendar/mini.blade.php +++ b/resources/views/components/calendar/mini.blade.php @@ -1,11 +1,31 @@ -@props(['class' => '']) +@props(['mini', 'view', 'nav', 'class' => '']) -
    +
    - July 2025 - Controls + {{ $nav['label'] }} +
    + {{-- preserve main calendar context for full-reload fallback --}} + + + {{-- nav buttons --}} + + {{-- --}} + +
    -
    +
    +
    diff --git a/resources/views/components/calendar/settings-menu.blade.php b/resources/views/components/calendar/settings-menu.blade.php new file mode 100644 index 0000000..db21a16 --- /dev/null +++ b/resources/views/components/calendar/settings-menu.blade.php @@ -0,0 +1,28 @@ +
    +
    + {{ __('General settings') }} + +
  • + + + Language and region + +
  • +
    +
    +
    + {{ __('Add a calendar') }} + +
  • + + + Subscribe to a calendar + +
  • +
    +
    +
    diff --git a/resources/views/components/input/checkbox-label.blade.php b/resources/views/components/input/checkbox-label.blade.php new file mode 100644 index 0000000..8be0b93 --- /dev/null +++ b/resources/views/components/input/checkbox-label.blade.php @@ -0,0 +1,14 @@ +@props([ + 'label' => '', // label text + 'labelclass' => '', // extra CSS classes for the label + 'checkclass' => '', // checkbox classes + 'name' => '', // checkbox name + 'value' => '', // checkbox value + 'style' => '', // raw style string for the checkbox + 'checked' => false // true/false or truthy value +]) + + diff --git a/resources/views/components/input/checkbox.blade.php b/resources/views/components/input/checkbox.blade.php new file mode 100644 index 0000000..368ba56 --- /dev/null +++ b/resources/views/components/input/checkbox.blade.php @@ -0,0 +1,14 @@ +@props([ + 'class' => '', // extra CSS classes + 'name' => '', // input name + 'value' => '', // input value + 'style' => '', // raw style string + 'checked' => false // true/false or truthy value +]) + +class($class) }} + @if($style !== '') style="{{ $style }}" @endif + @checked($checked) /> diff --git a/resources/views/components/input/text-label.blade.php b/resources/views/components/input/text-label.blade.php new file mode 100644 index 0000000..e486bb0 --- /dev/null +++ b/resources/views/components/input/text-label.blade.php @@ -0,0 +1,26 @@ +@props([ + 'label' => '', // label text + 'labelclass' => '', // extra CSS classes for the label + 'inputclass' => '', // input classes + 'name' => '', // input name + 'type' => 'text', // input type (text, url, etc) + 'value' => '', // input value + 'placeholder' => '', // placeholder text + 'style' => '', // raw style string for the input + 'required' => false, // true/false or truthy value + 'description' => '', // optional descriptive text below the input +]) + + diff --git a/resources/views/components/input/text.blade.php b/resources/views/components/input/text.blade.php new file mode 100644 index 0000000..a7b81cd --- /dev/null +++ b/resources/views/components/input/text.blade.php @@ -0,0 +1,17 @@ +@props([ + 'class' => '', // extra CSS classes + 'type' => 'text', // input type + 'name' => '', // input name + 'value' => '', // input value + 'placeholder' => '', // placeholder text + 'style' => '', // raw style string + 'required' => false, // true/false or truthy value +]) + +class($class) }} + @if($style !== '') style="{{ $style }}" @endif + @required($required) /> diff --git a/resources/views/layouts/guest.blade.php b/resources/views/layouts/guest.blade.php index 1c84e67..db27856 100644 --- a/resources/views/layouts/guest.blade.php +++ b/resources/views/layouts/guest.blade.php @@ -14,7 +14,7 @@

    {{ config('app.name', 'Kithkin') }}

    -
    +
    {{ $slot }}
    diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index 8b7723b..9bd037e 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -8,15 +8,15 @@ - + - - + + - - + + - +
    diff --git a/routes/web.php b/routes/web.php index 9fa04fb..f37cd15 100644 --- a/routes/web.php +++ b/routes/web.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\ProfileController; use App\Http\Controllers\BookController; use App\Http\Controllers\CalendarController; +use App\Http\Controllers\CalendarSettingsController; use App\Http\Controllers\CardController; use App\Http\Controllers\DavController; use App\Http\Controllers\EventController; @@ -51,6 +52,14 @@ Route::middleware('auth')->group(function () { ->prefix('calendar') ->name('calendar.') ->group(function () { + + // settings landing + Route::get('settings', [CalendarSettingsController::class, 'index'])->name('settings'); + + // settings / subscribe to a calendar + Route::get('settings/subscribe', [CalendarSettingsController::class, 'subscribeForm'])->name('settings.subscribe'); + Route::post('settings/subscribe', [CalendarSettingsController::class, 'subscribeStore'])->name('settings.subscribe.store'); + // remote calendar subscriptions Route::resource('subscriptions', SubscriptionController::class) ->except(['show']); // index, create, store, edit, update, destroy