From 47aafee97f66db1a25eec1a0a34b44830c233c8f Mon Sep 17 00:00:00 2001 From: Andy Newhouse Date: Thu, 14 May 2026 17:04:02 -0500 Subject: [PATCH 01/57] =?UTF-8?q?Just=20Keep=20Swimming=20=F0=9F=90=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Actions/Calendars/FetchCalendarEvents.php | 122 ++++++ app/Concerns/ProfileValidationRules.php | 7 + app/Models/CalendarFeed.php | 24 ++ app/Models/User.php | 10 +- composer.json | 1 + composer.lock | 240 +++++++++++- database/factories/CalendarFeedFactory.php | 26 ++ database/factories/UserFactory.php | 1 + ..._14_204921_create_calendar_feeds_table.php | 27 ++ ..._14_211119_add_timezone_to_users_table.php | 22 ++ .../dashboard/\342\232\241calendar.blade.php" | 349 ++++++++++++++++++ .../dashboard/\342\232\241calendar.test.php" | 236 ++++++++++++ resources/views/dashboard.blade.php | 17 +- .../views/flux/icon/cooking-pot.blade.php | 46 +++ resources/views/layouts/kiosk.blade.php | 31 ++ .../\342\232\241calendar/calendar.blade.php" | 69 ++++ .../kiosk/\342\232\241calendar/calendar.php" | 194 ++++++++++ .../\342\232\241calendar/calendar.test.php" | 8 + .../settings/\342\232\241profile.blade.php" | 14 + routes/web.php | 7 + tests/Feature/Settings/ProfileUpdateTest.php | 12 + 21 files changed, 1445 insertions(+), 18 deletions(-) create mode 100644 app/Actions/Calendars/FetchCalendarEvents.php create mode 100644 app/Models/CalendarFeed.php create mode 100644 database/factories/CalendarFeedFactory.php create mode 100644 database/migrations/2026_05_14_204921_create_calendar_feeds_table.php create mode 100644 database/migrations/2026_05_14_211119_add_timezone_to_users_table.php create mode 100644 "resources/views/components/dashboard/\342\232\241calendar.blade.php" create mode 100644 "resources/views/components/dashboard/\342\232\241calendar.test.php" create mode 100644 resources/views/flux/icon/cooking-pot.blade.php create mode 100644 resources/views/layouts/kiosk.blade.php create mode 100644 "resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241calendar/calendar.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" diff --git a/app/Actions/Calendars/FetchCalendarEvents.php b/app/Actions/Calendars/FetchCalendarEvents.php new file mode 100644 index 0000000..8421417 --- /dev/null +++ b/app/Actions/Calendars/FetchCalendarEvents.php @@ -0,0 +1,122 @@ + + */ + public function handle(CalendarFeed $feed, int $days = 30, ?CarbonImmutable $from = null): Collection + { + $body = Cache::remember( + key: "calendar-feed:{$feed->id}:" . md5($feed->url), + ttl: now()->addMinutes(15), + callback: fn () => Http::withHeaders([ + 'Accept' => 'text/calendar,text/plain,*/*', + 'User-Agent' => 'SunnyCalendar/1.0', + ])->timeout(10)->get($feed->url)->throw()->body(), + ); + + return $this->parse($body, $feed, $from ?? CarbonImmutable::now($this->timezoneName($feed)), $days); + } + + /** + * @return Collection + */ + public function parse(string $ics, CalendarFeed $feed, CarbonImmutable $from, int $days = 30): Collection + { + $timezone = new DateTimeZone($this->timezoneName($feed)); + $from = $from->setTimezone($timezone); + $until = $from->addDays($days); + $calendar = Reader::read($ics); + $expandedCalendar = $calendar->expand($from, $until, $timezone); + + return collect($expandedCalendar->select('VEVENT')) + ->filter(fn ($event) => $event instanceof VEvent && isset($event->DTSTART)) + ->map(fn (VEvent $event) => $this->eventData($event, $feed, $timezone)) + ->sortBy('starts_at') + ->values(); + } + + /** + * @return array{ + * feed_id: int, + * feed_name: string, + * feed_color: string, + * title: string, + * location: string|null, + * starts_at: CarbonImmutable, + * ends_at: CarbonImmutable|null, + * all_day: bool + * } + */ + private function eventData(VEvent $event, CalendarFeed $feed, DateTimeZone $timezone): array + { + return [ + 'feed_id' => $feed->id, + 'feed_name' => $feed->name, + 'feed_color' => $feed->color, + 'title' => blank((string) ($event->SUMMARY ?? '')) ? __('Untitled event') : (string) $event->SUMMARY, + 'location' => blank((string) ($event->LOCATION ?? '')) ? null : (string) $event->LOCATION, + 'starts_at' => $this->carbon($event->DTSTART->getDateTime($timezone), $timezone), + 'ends_at' => $this->endDate($event, $timezone), + 'all_day' => ! $event->DTSTART->hasTime(), + ]; + } + + private function endDate(VEvent $event, DateTimeZone $timezone): ?CarbonImmutable + { + if (isset($event->DTEND)) { + return $this->carbon($event->DTEND->getDateTime($timezone), $timezone); + } + + if (isset($event->DURATION)) { + return $this->carbon($event->DTSTART->getDateTime($timezone), $timezone) + ->add(DateTimeParser::parseDuration((string) $event->DURATION)); + } + + return null; + } + + private function carbon(DateTimeInterface $dateTime, DateTimeZone $timezone): CarbonImmutable + { + return CarbonImmutable::instance($dateTime)->setTimezone($timezone); + } + + private function timezoneName(CalendarFeed $feed): string + { + return $feed->user?->timezone ?: 'America/Chicago'; + } +} diff --git a/app/Concerns/ProfileValidationRules.php b/app/Concerns/ProfileValidationRules.php index 3005420..c2c7e49 100644 --- a/app/Concerns/ProfileValidationRules.php +++ b/app/Concerns/ProfileValidationRules.php @@ -16,6 +16,7 @@ protected function profileRules(?int $userId = null): array return [ 'name' => $this->nameRules(), 'email' => $this->emailRules($userId), + 'timezone' => $this->timezoneRules(), ]; } @@ -38,4 +39,10 @@ protected function emailRules(?int $userId = null): array : Rule::unique(User::class)->ignore($userId), ]; } + + /** @return array|string> */ + protected function timezoneRules(): array + { + return ['required', 'string', Rule::in(timezone_identifiers_list())]; + } } diff --git a/app/Models/CalendarFeed.php b/app/Models/CalendarFeed.php new file mode 100644 index 0000000..0cef1d9 --- /dev/null +++ b/app/Models/CalendarFeed.php @@ -0,0 +1,24 @@ + */ + use HasFactory; + + /** @return BelongsTo */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 431eb23..ff9afc5 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Str; @@ -18,7 +19,7 @@ use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Sanctum\HasApiTokens; -#[Fillable(['name', 'email', 'password', 'current_team_id'])] +#[Fillable(['name', 'email', 'password', 'current_team_id', 'timezone'])] #[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])] class User extends Authenticatable implements PasskeyUser { @@ -42,10 +43,17 @@ public function initials(): string public function purge(): void { $this->ownedTeams->each->purge(); + $this->calendarFeeds()->delete(); $this->teamMemberships()->delete(); $this->delete(); } + /** @return HasMany */ + public function calendarFeeds(): HasMany + { + return $this->hasMany(CalendarFeed::class); + } + /** @return array */ protected function casts(): array { diff --git a/composer.json b/composer.json index 701e722..fdd1cfb 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "livewire/flux-pro": "^2.13", "livewire/livewire": "^4.1", "nunomaduro/essentials": "^1.2", + "sabre/vobject": "^4.5", "spatie/simple-excel": "^3.9" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 9fb4819..2139677 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "791fd78bee159cb1d764bc55ac57367b", + "content-hash": "1bc20519780b28785b4ad3ae2a87155d", "packages": [ { "name": "aws/aws-crt-php", @@ -5090,6 +5090,244 @@ }, "time": "2025-12-14T04:43:48+00:00" }, + { + "name": "sabre/uri", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/uri.git", + "reference": "a926c749dddfb289b8a9b5218d16ac06affdc631" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/a926c749dddfb289b8a9b5218d16ac06affdc631", + "reference": "a926c749dddfb289b8a9b5218d16ac06affdc631", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.95", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^10.5", + "rector/rector": "^2.4" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\Uri\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "Functions for making sense out of URIs.", + "homepage": "http://sabre.io/uri/", + "keywords": [ + "rfc3986", + "uri", + "url" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/uri/issues", + "source": "https://github.com/fruux/sabre-uri" + }, + "time": "2026-04-26T04:19:03+00:00" + }, + { + "name": "sabre/vobject", + "version": "4.5.8", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/vobject.git", + "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1", + "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/xml": "^2.1 || ^3.0 || ^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpstan/phpstan": "^0.12 || ^1.12 || ^2.0", + "phpunit/php-invoker": "^2.0 || ^3.1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "hoa/bench": "If you would like to run the benchmark scripts" + }, + "bin": [ + "bin/vobject", + "bin/generate_vcards" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabre\\VObject\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Dominik Tobschall", + "email": "dominik@fruux.com", + "homepage": "http://tobschall.de/", + "role": "Developer" + }, + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net", + "homepage": "http://mnt.io/", + "role": "Developer" + } + ], + "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "homepage": "http://sabre.io/vobject/", + "keywords": [ + "availability", + "freebusy", + "iCalendar", + "ical", + "ics", + "jCal", + "jCard", + "recurrence", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868", + "vCalendar", + "vCard", + "vcf", + "xCal", + "xCard" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/vobject/issues", + "source": "https://github.com/fruux/sabre-vobject" + }, + "time": "2026-01-12T10:45:19+00:00" + }, + { + "name": "sabre/xml", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/xml.git", + "reference": "bec83cbe2f4e1e1ca4254ed8d7caa7e32cc74b05" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/bec83cbe2f4e1e1ca4254ed8d7caa7e32cc74b05", + "reference": "bec83cbe2f4e1e1ca4254ed8d7caa7e32cc74b05", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": "^8.2", + "sabre/uri": ">=2.0,<4.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.95", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^10.5", + "rector/rector": "^2.4" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ], + "psr-4": { + "Sabre\\Xml\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role": "Developer" + } + ], + "description": "sabre/xml is an XML library that you may not hate.", + "homepage": "https://sabre.io/xml/", + "keywords": [ + "XMLReader", + "XMLWriter", + "dom", + "xml" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/xml/issues", + "source": "https://github.com/fruux/sabre-xml" + }, + "time": "2026-04-27T10:56:01+00:00" + }, { "name": "saloonphp/saloon", "version": "v4.0.0", diff --git a/database/factories/CalendarFeedFactory.php b/database/factories/CalendarFeedFactory.php new file mode 100644 index 0000000..4023ce7 --- /dev/null +++ b/database/factories/CalendarFeedFactory.php @@ -0,0 +1,26 @@ + + */ +class CalendarFeedFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + return [ + 'user_id' => User::factory(), + 'name' => fake()->words(2, true), + 'url' => fake()->url() . '/calendar.ics', + 'color' => fake()->randomElement(['#2563eb', '#16a34a', '#dc2626', '#9333ea', '#ea580c', '#0891b2']), + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 00704a8..f6dd12c 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -25,6 +25,7 @@ public function definition(): array 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), + 'timezone' => 'America/Chicago', 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), 'two_factor_secret' => null, diff --git a/database/migrations/2026_05_14_204921_create_calendar_feeds_table.php b/database/migrations/2026_05_14_204921_create_calendar_feeds_table.php new file mode 100644 index 0000000..361e609 --- /dev/null +++ b/database/migrations/2026_05_14_204921_create_calendar_feeds_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->text('url'); + $table->string('color', 7)->default('#2563eb'); + $table->timestamps(); + + $table->index(['user_id', 'name']); + }); + } + + public function down(): void + { + Schema::dropIfExists('calendar_feeds'); + } +}; diff --git a/database/migrations/2026_05_14_211119_add_timezone_to_users_table.php b/database/migrations/2026_05_14_211119_add_timezone_to_users_table.php new file mode 100644 index 0000000..c118402 --- /dev/null +++ b/database/migrations/2026_05_14_211119_add_timezone_to_users_table.php @@ -0,0 +1,22 @@ +string('timezone')->default('America/Chicago')->after('email_verified_at'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('timezone'); + }); + } +}; diff --git "a/resources/views/components/dashboard/\342\232\241calendar.blade.php" "b/resources/views/components/dashboard/\342\232\241calendar.blade.php" new file mode 100644 index 0000000..6bdea41 --- /dev/null +++ "b/resources/views/components/dashboard/\342\232\241calendar.blade.php" @@ -0,0 +1,349 @@ + 'Blue', + '#16a34a' => 'Green', + '#dc2626' => 'Red', + '#9333ea' => 'Purple', + '#ea580c' => 'Orange', + '#0891b2' => 'Cyan', + '#ca8a04' => 'Gold', + '#4f46e5' => 'Indigo', + ]; + + public string $feedName = ''; + + public string $feedUrl = ''; + + public string $feedColor = self::DEFAULT_COLOR; + + public string $weekStartDate = ''; + + public function mount(): void + { + $this->weekStartDate = CarbonImmutable::now($this->timezoneName()) + ->startOfWeek(CarbonInterface::SUNDAY) + ->toDateString(); + } + + /** @return EloquentCollection */ + #[Computed] + public function feeds(): EloquentCollection + { + return Auth::user() + ->calendarFeeds() + ->orderBy('name') + ->get(); + } + + /** @return array> */ + #[Computed] + public function weekEvents(): array + { + $weekStartsAt = $this->weekStartsAt(); + + return $this->feeds + ->flatMap(function (CalendarFeed $feed) { + try { + return app(FetchCalendarEvents::class)->handle($feed, 7, $this->weekStartsAt()); + } catch (Throwable) { + return collect(); + } + }) + ->sortBy('starts_at') + ->take(12) + ->values() + ->all(); + } + + /** @return array>, is_today: bool}> */ + #[Computed] + public function weekDays(): array + { + $events = collect($this->weekEvents); + + return collect(range(0, 6)) + ->map(function (int $offset) use ($events): array { + $date = $this->weekStartsAt()->addDays($offset); + + return [ + 'date' => $date, + 'events' => $events + ->filter(fn (array $event) => $event['starts_at']->isSameDay($date)) + ->values() + ->all(), + 'is_today' => $date->isSameDay(CarbonImmutable::now($this->timezoneName())), + ]; + }) + ->all(); + } + + #[Computed] + public function weekLabel(): string + { + $weekStartsAt = $this->weekStartsAt(); + $weekEndsAt = $weekStartsAt->addDays(6); + + if ($weekStartsAt->isSameMonth($weekEndsAt)) { + return $weekStartsAt->format('M j') . ' - ' . $weekEndsAt->format('j, Y'); + } + + return $weekStartsAt->format('M j') . ' - ' . $weekEndsAt->format('M j, Y'); + } + + /** @return array */ + public function colorOptions(): array + { + return self::COLOR_OPTIONS; + } + + public function addFeed(): void + { + $this->feedUrl = rtrim($this->feedUrl, " \t\n\r\0\x0B,"); + + $validated = $this->validate([ + 'feedName' => ['nullable', 'string', 'max:255'], + 'feedUrl' => ['required', 'url', 'starts_with:http://,https://', 'max:2048'], + 'feedColor' => ['required', Rule::in(array_keys(self::COLOR_OPTIONS))], + ]); + + $url = $validated['feedUrl']; + $host = parse_url($url, PHP_URL_HOST); + + Auth::user()->calendarFeeds()->create([ + 'name' => filled($validated['feedName']) ? $validated['feedName'] : Str::headline((string) $host), + 'url' => $url, + 'color' => $validated['feedColor'], + ]); + + $this->reset('feedName', 'feedUrl'); + $this->feedColor = self::DEFAULT_COLOR; + unset($this->feeds, $this->weekEvents, $this->weekDays); + + Flux::toast(__('Calendar feed added.')); + } + + public function updateFeedColor(int $feedId, string $color): void + { + validator(['color' => $color], [ + 'color' => [Rule::in(array_keys(self::COLOR_OPTIONS))], + ])->validate(); + + Auth::user()->calendarFeeds()->whereKey($feedId)->update(['color' => $color]); + + unset($this->feeds, $this->weekEvents, $this->weekDays); + } + + public function deleteFeed(int $feedId): void + { + Auth::user()->calendarFeeds()->whereKey($feedId)->delete(); + + unset($this->feeds, $this->weekEvents, $this->weekDays); + + Flux::toast(__('Calendar feed removed.')); + } + + public function previousWeek(): void + { + $this->weekStartDate = $this->weekStartsAt()->subWeek()->toDateString(); + unset($this->weekEvents, $this->weekDays, $this->weekLabel); + } + + public function nextWeek(): void + { + $this->weekStartDate = $this->weekStartsAt()->addWeek()->toDateString(); + unset($this->weekEvents, $this->weekDays, $this->weekLabel); + } + + public function currentWeek(): void + { + $this->weekStartDate = CarbonImmutable::now($this->timezoneName()) + ->startOfWeek(CarbonInterface::SUNDAY) + ->toDateString(); + + unset($this->weekEvents, $this->weekDays, $this->weekLabel); + } + + private function weekStartsAt(): CarbonImmutable + { + return CarbonImmutable::parse($this->weekStartDate, $this->timezoneName()) + ->startOfWeek(CarbonInterface::SUNDAY); + } + + private function timezoneName(): string + { + return Auth::user()->timezone ?: 'America/Chicago'; + } +}; +?> + +
+
+
+ {{ __('Calendar') }} + + {{ __('Weekly events from your subscribed calendars') }} + +
+ +
+ + +
+ + {{ __('Add') }} + +
+
+ {{ __('Color') }} +
+ @foreach ($this->colorOptions() as $color => $label) + + @endforeach +
+ +
+ +
+ +
+
+
+
+ {{ __('Week') }} + {{ $this->weekLabel }} +
+ +
+ + {{ __('Today') }} + +
+
+ + @if ($this->feeds->isEmpty()) +
+ + + {{ __('Add a calendar feed to see weekly events.') }} + +
+ @else +
+ @foreach ($this->weekDays as $day) +
+
! $day['is_today'], + 'bg-blue-100 dark:bg-blue-900' => $day['is_today'], + ])> +
+
{{ $day['date']->format('D') }}
+
{{ $day['date']->format('M j') }}
+
+
+ +
+ @forelse ($day['events'] as $event) +
+
+ + {{ $event['all_day'] ? __('All day') : $event['starts_at']->format('g:i A') }} +
+ +
{{ $event['title'] }}
+
{{ $event['feed_name'] }}
+ + @if ($event['location']) +
{{ $event['location'] }}
+ @endif +
+ @empty + @endforelse +
+
+ @endforeach +
+ @endif +
+ +
+
+ {{ __('Feeds') }} +
+ +
+ @forelse ($this->feeds as $feed) +
+
+
+ +
{{ $feed->name }}
+
+
{{ parse_url($feed->url, PHP_URL_HOST) }}
+
+ @foreach ($this->colorOptions() as $color => $label) + + + + @endforeach +
+
+ + + + +
+ @empty +
+ {{ __('No feeds yet') }} +
+ @endforelse +
+
+
+
diff --git "a/resources/views/components/dashboard/\342\232\241calendar.test.php" "b/resources/views/components/dashboard/\342\232\241calendar.test.php" new file mode 100644 index 0000000..ab733f3 --- /dev/null +++ "b/resources/views/components/dashboard/\342\232\241calendar.test.php" @@ -0,0 +1,236 @@ +create()) + ->test('dashboard.calendar') + ->assertStatus(200); +}); + +it('adds calendar feeds for the authenticated user', function () { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test('dashboard.calendar') + ->set('feedName', 'Work') + ->set('feedUrl', 'https://calendar.example.com/basic.ics,') + ->set('feedColor', '#16a34a') + ->call('addFeed') + ->assertSet('feedName', '') + ->assertSet('feedUrl', '') + ->assertSet('feedColor', '#2563eb'); + + expect($user->calendarFeeds()->first()) + ->name->toBe('Work') + ->url->toBe('https://calendar.example.com/basic.ics') + ->color->toBe('#16a34a'); +}); + +it('requires a valid calendar feed url', function () { + Livewire::actingAs(User::factory()->create()) + ->test('dashboard.calendar') + ->set('feedUrl', 'not-a-url') + ->call('addFeed') + ->assertHasErrors(['feedUrl']); +}); + +it('requires a valid calendar feed color', function () { + Livewire::actingAs(User::factory()->create()) + ->test('dashboard.calendar') + ->set('feedUrl', 'https://calendar.example.com/basic.ics') + ->set('feedColor', '#ffffff') + ->call('addFeed') + ->assertHasErrors(['feedColor']); +}); + +it('updates only the authenticated users feed colors', function () { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $feed = CalendarFeed::factory()->for($user)->create(['color' => '#2563eb']); + $otherFeed = CalendarFeed::factory()->for($otherUser)->create(['color' => '#2563eb']); + + Livewire::actingAs($user) + ->test('dashboard.calendar') + ->call('updateFeedColor', $feed->id, '#dc2626') + ->call('updateFeedColor', $otherFeed->id, '#dc2626'); + + expect($feed->fresh()->color)->toBe('#dc2626') + ->and($otherFeed->fresh()->color)->toBe('#2563eb'); +}); + +it('removes only the authenticated users calendar feeds', function () { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $feed = CalendarFeed::factory()->for($user)->create(); + $otherFeed = CalendarFeed::factory()->for($otherUser)->create(); + + Livewire::actingAs($user) + ->test('dashboard.calendar') + ->call('deleteFeed', $feed->id) + ->call('deleteFeed', $otherFeed->id); + + expect($feed->fresh())->toBeNull() + ->and($otherFeed->fresh())->not->toBeNull(); +}); + +it('shows upcoming events from calendar feeds', function () { + CarbonImmutable::setTestNow('2026-05-14 09:00:00'); + Cache::store()->flush(); + Http::fake([ + 'calendar.example.com/*' => Http::response(calendarFixture()), + ]); + + $user = User::factory()->create(); + CalendarFeed::factory()->for($user)->create([ + 'name' => 'Work', + 'url' => 'https://calendar.example.com/basic.ics', + 'color' => '#9333ea', + ]); + + Livewire::actingAs($user) + ->test('dashboard.calendar') + ->assertSee('Design review') + ->assertSee('Work') + ->assertSee('#9333ea') + ->assertSee('10:00 AM') + ->assertSee('Office'); + + CarbonImmutable::setTestNow(); +}); + +it('shows events for the selected week and can navigate weeks', function () { + CarbonImmutable::setTestNow('2026-05-14 09:00:00'); + Cache::store()->flush(); + Http::fake([ + 'calendar.example.com/*' => Http::response(calendarFixture(<<create(); + CalendarFeed::factory()->for($user)->create([ + 'name' => 'Work', + 'url' => 'https://calendar.example.com/basic.ics', + ]); + + Livewire::actingAs($user) + ->test('dashboard.calendar') + ->assertSee('May 10 - 16, 2026') + ->assertSee('This week review') + ->assertDontSee('Next week planning') + ->call('nextWeek') + ->assertSee('May 17 - 23, 2026') + ->assertSee('Next week planning') + ->assertDontSee('This week review'); + + CarbonImmutable::setTestNow(); +}); + +it('expands simple recurring calendar events', function () { + $feed = CalendarFeed::factory()->make([ + 'id' => 10, + 'name' => 'Family', + ]); + + $events = app(FetchCalendarEvents::class)->parse( + calendarFixture(<<toHaveCount(3) + ->and($events->pluck('title')->all())->toBe(['Standup', 'Standup', 'Standup']); +}); + +it('honors excluded recurring calendar dates', function () { + $feed = CalendarFeed::factory()->make([ + 'id' => 10, + 'name' => 'Family', + ]); + + $events = app(FetchCalendarEvents::class)->parse( + calendarFixture(<<toHaveCount(2) + ->and($events->pluck('starts_at')->map->toDateString()->all())->toBe(['2026-05-14', '2026-05-28']); +}); + +it('converts calendar feed times to the users timezone', function () { + $user = User::factory()->create(['timezone' => 'America/New_York']); + $feed = CalendarFeed::factory()->for($user)->create([ + 'name' => 'Work', + 'url' => 'https://calendar.example.com/basic.ics', + ]); + + $events = app(FetchCalendarEvents::class)->parse( + calendarFixture(), + $feed, + CarbonImmutable::parse('2026-05-14 00:00:00', 'America/New_York'), + 30, + ); + + expect($events)->toHaveCount(1) + ->and($events->first()['starts_at']->timezoneName)->toBe('America/New_York') + ->and($events->first()['starts_at']->format('g:i A'))->toBe('11:00 AM'); +}); + +function calendarFixture(string $events = ''): string +{ + $events = $events ?: << -
-
-
- -
-
- -
-
- -
-
-
- -
-
+ diff --git a/resources/views/flux/icon/cooking-pot.blade.php b/resources/views/flux/icon/cooking-pot.blade.php new file mode 100644 index 0000000..7e732ea --- /dev/null +++ b/resources/views/flux/icon/cooking-pot.blade.php @@ -0,0 +1,46 @@ +@blaze(fold: true) + +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +if ($variant === 'solid') { + throw new \Exception('The "solid" variant is not supported in Lucide.'); +} + +$classes = Flux::classes('shrink-0') + ->add(match($variant) { + 'outline' => '[:where(&)]:size-6', + 'solid' => '[:where(&)]:size-6', + 'mini' => '[:where(&)]:size-5', + 'micro' => '[:where(&)]:size-4', + }); + +$strokeWidth = match ($variant) { + 'outline' => 2, + 'mini' => 2.25, + 'micro' => 2.5, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + + + diff --git a/resources/views/layouts/kiosk.blade.php b/resources/views/layouts/kiosk.blade.php new file mode 100644 index 0000000..6a69676 --- /dev/null +++ b/resources/views/layouts/kiosk.blade.php @@ -0,0 +1,31 @@ + + + + @include('layouts.partials.head') + + + + + + + + + Calendar + Routines + Chore Chart + Lists + Meal Planning + + + + + {{ $slot }} + + + @persist('toast') + + @endpersist + + @fluxScripts + + diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" new file mode 100644 index 0000000..bb82c6f --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" @@ -0,0 +1,69 @@ +
+
+
+ {{ __('Week') }} + {{ $this->weekLabel }} +
+ +
+ + {{ __('Today') }} + +
+
+ + @if ($this->feeds->isEmpty()) +
+ + + {{ __('Add a calendar feed to see weekly events.') }} + +
+ @else +
+ @foreach ($this->weekDays as $day) +
+
! $day['is_today'], + 'bg-blue-100 dark:bg-blue-900' => $day['is_today'], + ])> +
+
{{ $day['date']->format('D') }}
+
{{ $day['date']->format('M j') }}
+
+
+ +
+ @forelse ($day['events'] as $event) +
+
+ + {{ $event['all_day'] ? __('All day') : $event['starts_at']->format('g:i A') }} +
+ +
{{ $event['title'] }}
+
{{ $event['feed_name'] }}
+ + @if ($event['location']) +
{{ $event['location'] }}
+ @endif +
+ @empty + @endforelse +
+
+ @endforeach +
+ @endif +
diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" new file mode 100644 index 0000000..cebf4df --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" @@ -0,0 +1,194 @@ + 'Blue', + '#16a34a' => 'Green', + '#dc2626' => 'Red', + '#9333ea' => 'Purple', + '#ea580c' => 'Orange', + '#0891b2' => 'Cyan', + '#ca8a04' => 'Gold', + '#4f46e5' => 'Indigo', + ]; + + public string $feedName = ''; + + public string $feedUrl = ''; + + public string $feedColor = self::DEFAULT_COLOR; + + public string $weekStartDate = ''; + + public function mount(): void + { + $this->weekStartDate = CarbonImmutable::now($this->timezoneName()) + ->startOfWeek(CarbonInterface::SUNDAY) + ->toDateString(); + } + + /** @return EloquentCollection */ + #[Computed] + public function feeds(): EloquentCollection + { + return Auth::user() + ->calendarFeeds() + ->orderBy('name') + ->get(); + } + + /** @return array> */ + #[Computed] + public function weekEvents(): array + { + $weekStartsAt = $this->weekStartsAt(); + + return $this->feeds + ->flatMap(function (CalendarFeed $feed) { + try { + return app(FetchCalendarEvents::class)->handle($feed, 7, $this->weekStartsAt()); + } catch (Throwable) { + return collect(); + } + }) + ->sortBy('starts_at') + ->take(12) + ->values() + ->all(); + } + + /** @return array>, is_today: bool}> */ + #[Computed] + public function weekDays(): array + { + $events = collect($this->weekEvents); + + return collect(range(0, 6)) + ->map(function (int $offset) use ($events): array { + $date = $this->weekStartsAt()->addDays($offset); + + return [ + 'date' => $date, + 'events' => $events + ->filter(fn (array $event) => $event['starts_at']->isSameDay($date)) + ->values() + ->all(), + 'is_today' => $date->isSameDay(CarbonImmutable::now($this->timezoneName())), + ]; + }) + ->all(); + } + + #[Computed] + public function weekLabel(): string + { + $weekStartsAt = $this->weekStartsAt(); + $weekEndsAt = $weekStartsAt->addDays(6); + + if ($weekStartsAt->isSameMonth($weekEndsAt)) { + return $weekStartsAt->format('M j') . ' - ' . $weekEndsAt->format('j, Y'); + } + + return $weekStartsAt->format('M j') . ' - ' . $weekEndsAt->format('M j, Y'); + } + + /** @return array */ + public function colorOptions(): array + { + return self::COLOR_OPTIONS; + } + + public function addFeed(): void + { + $this->feedUrl = rtrim($this->feedUrl, " \t\n\r\0\x0B,"); + + $validated = $this->validate([ + 'feedName' => ['nullable', 'string', 'max:255'], + 'feedUrl' => ['required', 'url', 'starts_with:http://,https://', 'max:2048'], + 'feedColor' => ['required', Rule::in(array_keys(self::COLOR_OPTIONS))], + ]); + + $url = $validated['feedUrl']; + $host = parse_url($url, PHP_URL_HOST); + + Auth::user()->calendarFeeds()->create([ + 'name' => filled($validated['feedName']) ? $validated['feedName'] : Str::headline((string) $host), + 'url' => $url, + 'color' => $validated['feedColor'], + ]); + + $this->reset('feedName', 'feedUrl'); + $this->feedColor = self::DEFAULT_COLOR; + unset($this->feeds, $this->weekEvents, $this->weekDays); + + Flux::toast(__('Calendar feed added.')); + } + + public function updateFeedColor(int $feedId, string $color): void + { + validator(['color' => $color], [ + 'color' => [Rule::in(array_keys(self::COLOR_OPTIONS))], + ])->validate(); + + Auth::user()->calendarFeeds()->whereKey($feedId)->update(['color' => $color]); + + unset($this->feeds, $this->weekEvents, $this->weekDays); + } + + public function deleteFeed(int $feedId): void + { + Auth::user()->calendarFeeds()->whereKey($feedId)->delete(); + + unset($this->feeds, $this->weekEvents, $this->weekDays); + + Flux::toast(__('Calendar feed removed.')); + } + + public function previousWeek(): void + { + $this->weekStartDate = $this->weekStartsAt()->subWeek()->toDateString(); + unset($this->weekEvents, $this->weekDays, $this->weekLabel); + } + + public function nextWeek(): void + { + $this->weekStartDate = $this->weekStartsAt()->addWeek()->toDateString(); + unset($this->weekEvents, $this->weekDays, $this->weekLabel); + } + + public function currentWeek(): void + { + $this->weekStartDate = CarbonImmutable::now($this->timezoneName()) + ->startOfWeek(CarbonInterface::SUNDAY) + ->toDateString(); + + unset($this->weekEvents, $this->weekDays, $this->weekLabel); + } + + private function weekStartsAt(): CarbonImmutable + { + return CarbonImmutable::parse($this->weekStartDate, $this->timezoneName()) + ->startOfWeek(CarbonInterface::SUNDAY); + } + + private function timezoneName(): string + { + return Auth::user()->timezone ?: 'America/Chicago'; + } +}; diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" new file mode 100644 index 0000000..25eec7c --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" @@ -0,0 +1,8 @@ +assertStatus(200); +}); diff --git "a/resources/views/pages/settings/\342\232\241profile.blade.php" "b/resources/views/pages/settings/\342\232\241profile.blade.php" index 75c6cf2..f30d7f2 100644 --- "a/resources/views/pages/settings/\342\232\241profile.blade.php" +++ "b/resources/views/pages/settings/\342\232\241profile.blade.php" @@ -14,11 +14,13 @@ public string $name = ''; public string $email = ''; + public string $timezone = 'America/Chicago'; public function mount(): void { $this->name = Auth::user()->name; $this->email = Auth::user()->email; + $this->timezone = Auth::user()->timezone; } public function updateProfileInformation(): void @@ -65,6 +67,12 @@ public function showDeleteUser(): bool return ! Auth::user() instanceof MustVerifyEmail || (Auth::user() instanceof MustVerifyEmail && Auth::user()->hasVerifiedEmail()); } + + /** @return array */ + public function timezones(): array + { + return \DateTimeZone::listIdentifiers(); + } }; ?>
@@ -98,6 +106,12 @@ public function showDeleteUser(): bool @endif + + @foreach ($this->timezones() as $timezoneOption) + {{ str_replace('_', ' ', $timezoneOption) }} + @endforeach + +
diff --git a/routes/web.php b/routes/web.php index 340ee19..c7ef328 100644 --- a/routes/web.php +++ b/routes/web.php @@ -19,3 +19,10 @@ require __DIR__ . '/inventory.php'; require __DIR__ . '/recipes.php'; require __DIR__ . '/settings.php'; + +Route::prefix('{current_team}/kiosk') + ->middleware(['auth', 'verified', EnsureTeamMembership::class]) + ->name('kiosk') + ->group(function (): void { + Route::livewire('/calendar', 'pages::kiosk.calendar')->name('.calendar'); + }); diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php index d5f703a..b5eec28 100644 --- a/tests/Feature/Settings/ProfileUpdateTest.php +++ b/tests/Feature/Settings/ProfileUpdateTest.php @@ -16,15 +16,27 @@ ->test('pages::settings.profile') ->set('name', 'Test User') ->set('email', 'test@example.com') + ->set('timezone', 'America/New_York') ->call('updateProfileInformation') ->assertHasNoErrors(); expect($user->fresh()) ->name->toEqual('Test User') ->email->toEqual('test@example.com') + ->timezone->toEqual('America/New_York') ->email_verified_at->toBeNull(); }); +test('profile timezone must be valid', function () { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test('pages::settings.profile') + ->set('timezone', 'Not/AZone') + ->call('updateProfileInformation') + ->assertHasErrors(['timezone']); +}); + test('email verification status is unchanged when email address is unchanged', function () { $user = User::factory()->create(); From ad59b22e04201fb43fd5b26f8c74cd1130dc1943 Mon Sep 17 00:00:00 2001 From: techenby <6541180+techenby@users.noreply.github.com> Date: Thu, 14 May 2026 22:05:59 +0000 Subject: [PATCH 02/57] Rector fixes --- .../components/dashboard/\342\232\241calendar.blade.php" | 2 +- .../components/dashboard/\342\232\241calendar.test.php" | 6 +++--- .../views/pages/kiosk/\342\232\241calendar/calendar.php" | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git "a/resources/views/components/dashboard/\342\232\241calendar.blade.php" "b/resources/views/components/dashboard/\342\232\241calendar.blade.php" index 6bdea41..ec199d5 100644 --- "a/resources/views/components/dashboard/\342\232\241calendar.blade.php" +++ "b/resources/views/components/dashboard/\342\232\241calendar.blade.php" @@ -60,7 +60,7 @@ public function weekEvents(): array return $this->feeds ->flatMap(function (CalendarFeed $feed) { try { - return app(FetchCalendarEvents::class)->handle($feed, 7, $this->weekStartsAt()); + return resolve(FetchCalendarEvents::class)->handle($feed, 7, $this->weekStartsAt()); } catch (Throwable) { return collect(); } diff --git "a/resources/views/components/dashboard/\342\232\241calendar.test.php" "b/resources/views/components/dashboard/\342\232\241calendar.test.php" index ab733f3..feca966 100644 --- "a/resources/views/components/dashboard/\342\232\241calendar.test.php" +++ "b/resources/views/components/dashboard/\342\232\241calendar.test.php" @@ -150,7 +150,7 @@ 'name' => 'Family', ]); - $events = app(FetchCalendarEvents::class)->parse( + $events = resolve(FetchCalendarEvents::class)->parse( calendarFixture(<< 'Family', ]); - $events = app(FetchCalendarEvents::class)->parse( + $events = resolve(FetchCalendarEvents::class)->parse( calendarFixture(<< 'https://calendar.example.com/basic.ics', ]); - $events = app(FetchCalendarEvents::class)->parse( + $events = resolve(FetchCalendarEvents::class)->parse( calendarFixture(), $feed, CarbonImmutable::parse('2026-05-14 00:00:00', 'America/New_York'), diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" index cebf4df..13d49c6 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" @@ -62,7 +62,7 @@ public function weekEvents(): array return $this->feeds ->flatMap(function (CalendarFeed $feed) { try { - return app(FetchCalendarEvents::class)->handle($feed, 7, $this->weekStartsAt()); + return resolve(FetchCalendarEvents::class)->handle($feed, 7, $this->weekStartsAt()); } catch (Throwable) { return collect(); } From 8795366b7b94c50f744dbff6ada0126369471edb Mon Sep 17 00:00:00 2001 From: Andy Newhouse Date: Thu, 14 May 2026 17:13:06 -0500 Subject: [PATCH 03/57] Stub out other pages --- resources/views/layouts/kiosk.blade.php | 10 +++++----- .../kiosk/\342\232\241calendar/calendar.test.php" | 4 +++- .../\342\232\241chore-chart/chore-chart.blade.php" | 9 +++++++++ .../kiosk/\342\232\241chore-chart/chore-chart.php" | 9 +++++++++ .../\342\232\241chore-chart/chore-chart.test.php" | 11 +++++++++++ .../pages/kiosk/\342\232\241lists/lists.blade.php" | 9 +++++++++ .../views/pages/kiosk/\342\232\241lists/lists.php" | 9 +++++++++ .../pages/kiosk/\342\232\241lists/lists.test.php" | 11 +++++++++++ .../meal-planning.blade.php" | 9 +++++++++ .../\342\232\241meal-planning/meal-planning.php" | 9 +++++++++ .../\342\232\241meal-planning/meal-planning.test.php" | 11 +++++++++++ .../kiosk/\342\232\241routines/routines.blade.php" | 9 +++++++++ .../pages/kiosk/\342\232\241routines/routines.php" | 9 +++++++++ .../kiosk/\342\232\241routines/routines.test.php" | 11 +++++++++++ routes/web.php | 6 +++++- 15 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 "resources/views/pages/kiosk/\342\232\241chore-chart/chore-chart.blade.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241chore-chart/chore-chart.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241chore-chart/chore-chart.test.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241lists/lists.blade.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241lists/lists.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241lists/lists.test.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241meal-planning/meal-planning.blade.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241meal-planning/meal-planning.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241meal-planning/meal-planning.test.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241routines/routines.blade.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241routines/routines.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241routines/routines.test.php" diff --git a/resources/views/layouts/kiosk.blade.php b/resources/views/layouts/kiosk.blade.php index 6a69676..046d8da 100644 --- a/resources/views/layouts/kiosk.blade.php +++ b/resources/views/layouts/kiosk.blade.php @@ -10,11 +10,11 @@ - Calendar - Routines - Chore Chart - Lists - Meal Planning + Calendar + Routines + Chore Chart + Lists + Meal Planning diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" index 25eec7c..5cf9d82 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" @@ -1,8 +1,10 @@ create()) + ->test('pages::kiosk.calendar') ->assertStatus(200); }); diff --git "a/resources/views/pages/kiosk/\342\232\241chore-chart/chore-chart.blade.php" "b/resources/views/pages/kiosk/\342\232\241chore-chart/chore-chart.blade.php" new file mode 100644 index 0000000..e0c7043 --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241chore-chart/chore-chart.blade.php" @@ -0,0 +1,9 @@ +
+
+ + {{ __('Chore Chart') }} + + {{ __('Household chores and assignments will live here.') }} + +
+
diff --git "a/resources/views/pages/kiosk/\342\232\241chore-chart/chore-chart.php" "b/resources/views/pages/kiosk/\342\232\241chore-chart/chore-chart.php" new file mode 100644 index 0000000..80b1f7b --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241chore-chart/chore-chart.php" @@ -0,0 +1,9 @@ +create()) + ->test('pages::kiosk.chore-chart') + ->assertStatus(200) + ->assertSee('Chore Chart'); +}); diff --git "a/resources/views/pages/kiosk/\342\232\241lists/lists.blade.php" "b/resources/views/pages/kiosk/\342\232\241lists/lists.blade.php" new file mode 100644 index 0000000..6a78ce6 --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241lists/lists.blade.php" @@ -0,0 +1,9 @@ +
+
+ + {{ __('Lists') }} + + {{ __('Shared lists will live here.') }} + +
+
diff --git "a/resources/views/pages/kiosk/\342\232\241lists/lists.php" "b/resources/views/pages/kiosk/\342\232\241lists/lists.php" new file mode 100644 index 0000000..80b1f7b --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241lists/lists.php" @@ -0,0 +1,9 @@ +create()) + ->test('pages::kiosk.lists') + ->assertStatus(200) + ->assertSee('Lists'); +}); diff --git "a/resources/views/pages/kiosk/\342\232\241meal-planning/meal-planning.blade.php" "b/resources/views/pages/kiosk/\342\232\241meal-planning/meal-planning.blade.php" new file mode 100644 index 0000000..ec35fe1 --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241meal-planning/meal-planning.blade.php" @@ -0,0 +1,9 @@ +
+
+ + {{ __('Meal Planning') }} + + {{ __('Meal plans and dinner ideas will live here.') }} + +
+
diff --git "a/resources/views/pages/kiosk/\342\232\241meal-planning/meal-planning.php" "b/resources/views/pages/kiosk/\342\232\241meal-planning/meal-planning.php" new file mode 100644 index 0000000..80b1f7b --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241meal-planning/meal-planning.php" @@ -0,0 +1,9 @@ +create()) + ->test('pages::kiosk.meal-planning') + ->assertStatus(200) + ->assertSee('Meal Planning'); +}); diff --git "a/resources/views/pages/kiosk/\342\232\241routines/routines.blade.php" "b/resources/views/pages/kiosk/\342\232\241routines/routines.blade.php" new file mode 100644 index 0000000..c1cc933 --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241routines/routines.blade.php" @@ -0,0 +1,9 @@ +
+
+ + {{ __('Routines') }} + + {{ __('Routine tracking will live here.') }} + +
+
diff --git "a/resources/views/pages/kiosk/\342\232\241routines/routines.php" "b/resources/views/pages/kiosk/\342\232\241routines/routines.php" new file mode 100644 index 0000000..80b1f7b --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241routines/routines.php" @@ -0,0 +1,9 @@ +create()) + ->test('pages::kiosk.routines') + ->assertStatus(200) + ->assertSee('Routines'); +}); diff --git a/routes/web.php b/routes/web.php index c7ef328..0bcec20 100644 --- a/routes/web.php +++ b/routes/web.php @@ -24,5 +24,9 @@ ->middleware(['auth', 'verified', EnsureTeamMembership::class]) ->name('kiosk') ->group(function (): void { - Route::livewire('/calendar', 'pages::kiosk.calendar')->name('.calendar'); + Route::livewire('calendar', 'pages::kiosk.calendar')->name('.calendar'); + Route::livewire('routines', 'pages::kiosk.routines')->name('.routines'); + Route::livewire('chore-chart', 'pages::kiosk.chore-chart')->name('.chore-chart'); + Route::livewire('lists', 'pages::kiosk.lists')->name('.lists'); + Route::livewire('meal-planning', 'pages::kiosk.meal-planning')->name('.meal-planning'); }); From 71551d4fff8da09e1e535e7a74dc0124276ba421 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 12:45:55 -0500 Subject: [PATCH 04/57] =?UTF-8?q?Just=20Keep=20Swimming=20=F0=9F=90=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Enums/CalendarColor.php | 15 +++ app/Livewire/Forms/Kiosk/SettingsForm.php | 33 ++++++ ...14_211119_add_timezone_to_teams_table.php} | 10 +- .../components/kiosk/sidebar/index.blade.php | 17 +++ .../components/kiosk/sidebar/item.blade.php | 15 +++ .../weather-tile.blade.php" | 13 +++ .../weather-tile.php" | 8 ++ .../weather-tile.test.php" | 8 ++ resources/views/layouts/kiosk.blade.php | 24 ++--- .../\342\232\241calendar/calendar.blade.php" | 29 +++-- .../kiosk/\342\232\241calendar/calendar.php" | 101 +++--------------- .../\342\232\241settings/settings.blade.php" | 19 ++++ .../kiosk/\342\232\241settings/settings.php" | 21 ++++ .../\342\232\241settings/settings.test.php" | 8 ++ routes/web.php | 1 + 15 files changed, 206 insertions(+), 116 deletions(-) create mode 100644 app/Enums/CalendarColor.php create mode 100644 app/Livewire/Forms/Kiosk/SettingsForm.php rename database/migrations/{2026_05_14_211119_add_timezone_to_users_table.php => 2026_05_14_211119_add_timezone_to_teams_table.php} (51%) create mode 100644 resources/views/components/kiosk/sidebar/index.blade.php create mode 100644 resources/views/components/kiosk/sidebar/item.blade.php create mode 100644 "resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.blade.php" create mode 100644 "resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" create mode 100644 "resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.test.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241settings/settings.blade.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241settings/settings.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241settings/settings.test.php" diff --git a/app/Enums/CalendarColor.php b/app/Enums/CalendarColor.php new file mode 100644 index 0000000..9184c0b --- /dev/null +++ b/app/Enums/CalendarColor.php @@ -0,0 +1,15 @@ +editingTeam = $team; + $this->timezone = $team->timezone; + $this->week_start = $team->week_start; + } + + public function save() + { + $data = $this->validate(); + + $this->editingTeam->query()->update($data); + } +} diff --git a/database/migrations/2026_05_14_211119_add_timezone_to_users_table.php b/database/migrations/2026_05_14_211119_add_timezone_to_teams_table.php similarity index 51% rename from database/migrations/2026_05_14_211119_add_timezone_to_users_table.php rename to database/migrations/2026_05_14_211119_add_timezone_to_teams_table.php index c118402..ad39cf5 100644 --- a/database/migrations/2026_05_14_211119_add_timezone_to_users_table.php +++ b/database/migrations/2026_05_14_211119_add_timezone_to_teams_table.php @@ -2,21 +2,23 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { - Schema::table('users', function (Blueprint $table) { - $table->string('timezone')->default('America/Chicago')->after('email_verified_at'); + Schema::table('teams', function (Blueprint $table) { + $table->string('timezone')->default('America/Chicago')->after('is_personal'); + $table->integer('week_start')->default(Carbon::SUNDAY)->after('timezone'); }); } public function down(): void { - Schema::table('users', function (Blueprint $table) { - $table->dropColumn('timezone'); + Schema::table('teams', function (Blueprint $table) { + $table->dropColumns(['timezone', 'week_start']); }); } }; diff --git a/resources/views/components/kiosk/sidebar/index.blade.php b/resources/views/components/kiosk/sidebar/index.blade.php new file mode 100644 index 0000000..5f99995 --- /dev/null +++ b/resources/views/components/kiosk/sidebar/index.blade.php @@ -0,0 +1,17 @@ +@blaze(fold: true) + +@php + $classes = Flux::classes('[grid-area:sidebar]') + ->add('z-1 flex flex-col [:where(&)]:w-32') + ->add('bg-zinc-50 dark:bg-zinc-900 border-r border-zinc-200 dark:border-zinc-700') + ->add('divide-y divide-zinc-200 dark:divide-zinc-700') + // ->add('data-flux-sidebar-collapsed-desktop:w-14 data-flux-sidebar-collapsed-desktop:px-2') + // ->add('data-flux-sidebar-collapsed-desktop:cursor-e-resize rtl:data-flux-sidebar-collapsed-desktop:cursor-w-resize') + ; +@endphp + +
class($classes) }} +> + {{ $slot }} +
diff --git a/resources/views/components/kiosk/sidebar/item.blade.php b/resources/views/components/kiosk/sidebar/item.blade.php new file mode 100644 index 0000000..204043d --- /dev/null +++ b/resources/views/components/kiosk/sidebar/item.blade.php @@ -0,0 +1,15 @@ +@props(['icon']) + +@php + $classes = Flux::classes() + ->add('flex flex-col items-center justify-center p-2 h-20') + ->add('data-current:text-(--color-accent-content) hover:data-current:text-(--color-accent-content)') + ->add('data-current:bg-white dark:data-current:bg-white/[7%]') + ->add('last:border-b last:border-zinc-200 last:dark:border-zinc-700') + ; +@endphp + +merge(['class' => $classes]) }}> + + {{ $slot }} + diff --git "a/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.blade.php" "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.blade.php" new file mode 100644 index 0000000..bcea664 --- /dev/null +++ "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.blade.php" @@ -0,0 +1,13 @@ +
+
+
+ Chicago + 63ยฐ +
+
+ + 73ยฐ + 55ยฐ +
+
+
diff --git "a/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" new file mode 100644 index 0000000..008929e --- /dev/null +++ "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" @@ -0,0 +1,8 @@ +assertStatus(200); +}); diff --git a/resources/views/layouts/kiosk.blade.php b/resources/views/layouts/kiosk.blade.php index 046d8da..114af06 100644 --- a/resources/views/layouts/kiosk.blade.php +++ b/resources/views/layouts/kiosk.blade.php @@ -4,21 +4,17 @@ @include('layouts.partials.head') - - - - + + + {{ __('Calendar') }} + {{ __('Routines') }} + {{ __('Chores') }} + {{ __('Lists') }} + {{ __('Meals') }} + {{ __('Settings') }} + - - Calendar - Routines - Chore Chart - Lists - Meal Planning - - - - + {{ $slot }} diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" index bb82c6f..e8f3671 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" @@ -1,11 +1,16 @@ -
-
+
+
- {{ __('Week') }} - {{ $this->weekLabel }} + {{ $this->nowLabel }}
+ + + + + + {{ __('Today') }} @@ -20,9 +25,17 @@
@else -
+
+ + @foreach ($this->feeds as $feed) + + @endforeach + +
+ +
@foreach ($this->weekDays as $day) -
+
! $day['is_today'], @@ -48,12 +61,10 @@ class="rounded-md border border-zinc-200 bg-white p-2 text-sm shadow-xs dark:bor style="border-left: 4px solid {{ $event['feed_color'] }}" >
- {{ $event['all_day'] ? __('All day') : $event['starts_at']->format('g:i A') }}
-
{{ $event['title'] }}
-
{{ $event['feed_name'] }}
+
{{ $event['title'] }}
@if ($event['location'])
{{ $event['location'] }}
diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" index 13d49c6..95c32f3 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" @@ -1,46 +1,31 @@ 'Blue', - '#16a34a' => 'Green', - '#dc2626' => 'Red', - '#9333ea' => 'Purple', - '#ea580c' => 'Orange', - '#0891b2' => 'Cyan', - '#ca8a04' => 'Gold', - '#4f46e5' => 'Indigo', - ]; - - public string $feedName = ''; - - public string $feedUrl = ''; + public string $weekStartDate = ''; - public string $feedColor = self::DEFAULT_COLOR; + public string $format = 'week'; - public string $weekStartDate = ''; + public array $selectedFeeds = []; public function mount(): void { $this->weekStartDate = CarbonImmutable::now($this->timezoneName()) - ->startOfWeek(CarbonInterface::SUNDAY) + ->startOfWeek(Auth::user()->currentTeam->week_start) ->toDateString(); + + $this->selectedFeeds = $this->feeds->pluck('id')->toArray(); } /** @return EloquentCollection */ @@ -49,7 +34,6 @@ public function feeds(): EloquentCollection { return Auth::user() ->calendarFeeds() - ->orderBy('name') ->get(); } @@ -57,9 +41,8 @@ public function feeds(): EloquentCollection #[Computed] public function weekEvents(): array { - $weekStartsAt = $this->weekStartsAt(); - return $this->feeds + ->whereIn('id', $this->selectedFeeds) ->flatMap(function (CalendarFeed $feed) { try { return resolve(FetchCalendarEvents::class)->handle($feed, 7, $this->weekStartsAt()); @@ -68,7 +51,6 @@ public function weekEvents(): array } }) ->sortBy('starts_at') - ->take(12) ->values() ->all(); } @@ -96,68 +78,9 @@ public function weekDays(): array } #[Computed] - public function weekLabel(): string - { - $weekStartsAt = $this->weekStartsAt(); - $weekEndsAt = $weekStartsAt->addDays(6); - - if ($weekStartsAt->isSameMonth($weekEndsAt)) { - return $weekStartsAt->format('M j') . ' - ' . $weekEndsAt->format('j, Y'); - } - - return $weekStartsAt->format('M j') . ' - ' . $weekEndsAt->format('M j, Y'); - } - - /** @return array */ - public function colorOptions(): array - { - return self::COLOR_OPTIONS; - } - - public function addFeed(): void + public function nowLabel(): string { - $this->feedUrl = rtrim($this->feedUrl, " \t\n\r\0\x0B,"); - - $validated = $this->validate([ - 'feedName' => ['nullable', 'string', 'max:255'], - 'feedUrl' => ['required', 'url', 'starts_with:http://,https://', 'max:2048'], - 'feedColor' => ['required', Rule::in(array_keys(self::COLOR_OPTIONS))], - ]); - - $url = $validated['feedUrl']; - $host = parse_url($url, PHP_URL_HOST); - - Auth::user()->calendarFeeds()->create([ - 'name' => filled($validated['feedName']) ? $validated['feedName'] : Str::headline((string) $host), - 'url' => $url, - 'color' => $validated['feedColor'], - ]); - - $this->reset('feedName', 'feedUrl'); - $this->feedColor = self::DEFAULT_COLOR; - unset($this->feeds, $this->weekEvents, $this->weekDays); - - Flux::toast(__('Calendar feed added.')); - } - - public function updateFeedColor(int $feedId, string $color): void - { - validator(['color' => $color], [ - 'color' => [Rule::in(array_keys(self::COLOR_OPTIONS))], - ])->validate(); - - Auth::user()->calendarFeeds()->whereKey($feedId)->update(['color' => $color]); - - unset($this->feeds, $this->weekEvents, $this->weekDays); - } - - public function deleteFeed(int $feedId): void - { - Auth::user()->calendarFeeds()->whereKey($feedId)->delete(); - - unset($this->feeds, $this->weekEvents, $this->weekDays); - - Flux::toast(__('Calendar feed removed.')); + return CarbonImmutable::now($this->timezoneName())->format('D, M j g:i A'); } public function previousWeek(): void @@ -184,11 +107,11 @@ public function currentWeek(): void private function weekStartsAt(): CarbonImmutable { return CarbonImmutable::parse($this->weekStartDate, $this->timezoneName()) - ->startOfWeek(CarbonInterface::SUNDAY); + ->startOfWeek(Auth::user()->currentTeam->week_start); } private function timezoneName(): string { - return Auth::user()->timezone ?: 'America/Chicago'; + return Auth::user()->currentTeam->timezone ?: 'America/Chicago'; } }; diff --git "a/resources/views/pages/kiosk/\342\232\241settings/settings.blade.php" "b/resources/views/pages/kiosk/\342\232\241settings/settings.blade.php" new file mode 100644 index 0000000..fff10e6 --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241settings/settings.blade.php" @@ -0,0 +1,19 @@ +
+ + @foreach (DateTimeZone::listIdentifiers() as $timezoneOption) + {{ str_replace('_', ' ', $timezoneOption) }} + @endforeach + + + + {{ __('Sunday') }} + {{ __('Monday') }} + {{ __('Tuesday') }} + {{ __('Wednesday') }} + {{ __('Thursday') }} + {{ __('Friday') }} + {{ __('Saturday') }} + + + Save +
diff --git "a/resources/views/pages/kiosk/\342\232\241settings/settings.php" "b/resources/views/pages/kiosk/\342\232\241settings/settings.php" new file mode 100644 index 0000000..48e79e2 --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241settings/settings.php" @@ -0,0 +1,21 @@ +form->load(Auth::user()->currentTeam); + } + + public function save() + { + $this->form->save(); + } +}; diff --git "a/resources/views/pages/kiosk/\342\232\241settings/settings.test.php" "b/resources/views/pages/kiosk/\342\232\241settings/settings.test.php" new file mode 100644 index 0000000..34b2aa5 --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241settings/settings.test.php" @@ -0,0 +1,8 @@ +assertStatus(200); +}); diff --git a/routes/web.php b/routes/web.php index 0bcec20..182863e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -29,4 +29,5 @@ Route::livewire('chore-chart', 'pages::kiosk.chore-chart')->name('.chore-chart'); Route::livewire('lists', 'pages::kiosk.lists')->name('.lists'); Route::livewire('meal-planning', 'pages::kiosk.meal-planning')->name('.meal-planning'); + Route::livewire('settings', 'pages::kiosk.settings')->name('.settings'); }); From 3c66e254d150f6a6381e01897daebefc80998c03 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 12:48:04 -0500 Subject: [PATCH 05/57] Remove timezone from factory --- database/factories/UserFactory.php | 1 - 1 file changed, 1 deletion(-) diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index f6dd12c..00704a8 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -25,7 +25,6 @@ public function definition(): array 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), - 'timezone' => 'America/Chicago', 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), 'two_factor_secret' => null, From 5b36d4d6d23ae371ad7df3fa77633b4fcf923431 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 13:07:28 -0500 Subject: [PATCH 06/57] =?UTF-8?q?Just=20Keep=20Swimming=20=F0=9F=90=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Actions/Calendars/FetchCalendarEvents.php | 33 ++++++++++ .../dashboard/\342\232\241calendar.test.php" | 60 +++++++++++++++++++ .../\342\232\241calendar/calendar.blade.php" | 7 ++- 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/app/Actions/Calendars/FetchCalendarEvents.php b/app/Actions/Calendars/FetchCalendarEvents.php index 8421417..ab4cedc 100644 --- a/app/Actions/Calendars/FetchCalendarEvents.php +++ b/app/Actions/Calendars/FetchCalendarEvents.php @@ -27,6 +27,7 @@ class FetchCalendarEvents * starts_at: CarbonImmutable, * ends_at: CarbonImmutable|null, * all_day: bool + * response_status: string|null * }> */ public function handle(CalendarFeed $feed, int $days = 30, ?CarbonImmutable $from = null): Collection @@ -53,6 +54,7 @@ public function handle(CalendarFeed $feed, int $days = 30, ?CarbonImmutable $fro * starts_at: CarbonImmutable, * ends_at: CarbonImmutable|null, * all_day: bool + * response_status: string|null * }> */ public function parse(string $ics, CalendarFeed $feed, CarbonImmutable $from, int $days = 30): Collection @@ -80,10 +82,13 @@ public function parse(string $ics, CalendarFeed $feed, CarbonImmutable $from, in * starts_at: CarbonImmutable, * ends_at: CarbonImmutable|null, * all_day: bool + * response_status: string|null * } */ private function eventData(VEvent $event, CalendarFeed $feed, DateTimeZone $timezone): array { + $responseStatus = $this->responseStatus($event, $feed); + return [ 'feed_id' => $feed->id, 'feed_name' => $feed->name, @@ -93,6 +98,7 @@ private function eventData(VEvent $event, CalendarFeed $feed, DateTimeZone $time 'starts_at' => $this->carbon($event->DTSTART->getDateTime($timezone), $timezone), 'ends_at' => $this->endDate($event, $timezone), 'all_day' => ! $event->DTSTART->hasTime(), + 'response_status' => $responseStatus, ]; } @@ -115,6 +121,33 @@ private function carbon(DateTimeInterface $dateTime, DateTimeZone $timezone): Ca return CarbonImmutable::instance($dateTime)->setTimezone($timezone); } + private function responseStatus(VEvent $event, CalendarFeed $feed): ?string + { + if (! isset($event->ATTENDEE)) { + return null; + } + + $email = mb_strtolower((string) $feed->user?->email); + + if ($email === '') { + return null; + } + + foreach ($event->ATTENDEE as $attendee) { + $attendeeEmail = mb_strtolower(preg_replace('/^mailto:/i', '', (string) $attendee->getValue())); + + if ($attendeeEmail !== $email) { + continue; + } + + return isset($attendee['PARTSTAT']) + ? strtoupper((string) $attendee['PARTSTAT']) + : null; + } + + return null; + } + private function timezoneName(CalendarFeed $feed): string { return $feed->user?->timezone ?: 'America/Chicago'; diff --git "a/resources/views/components/dashboard/\342\232\241calendar.test.php" "b/resources/views/components/dashboard/\342\232\241calendar.test.php" index feca966..d7e79b4 100644 --- "a/resources/views/components/dashboard/\342\232\241calendar.test.php" +++ "b/resources/views/components/dashboard/\342\232\241calendar.test.php" @@ -214,6 +214,66 @@ ->and($events->first()['starts_at']->format('g:i A'))->toBe('11:00 AM'); }); +it('marks events pending response when the feed owner attendee needs action', function () { + $feed = new CalendarFeed([ + 'name' => 'Work', + 'url' => 'https://calendar.example.com/basic.ics', + 'color' => '#2563eb', + ]); + $feed->id = 10; + $feed->setRelation('user', User::factory()->make([ + 'email' => 'andy@example.com', + 'timezone' => 'America/Chicago', + ])); + + $events = resolve(FetchCalendarEvents::class)->parse( + calendarFixture(<<first()['response_status'])->toBe('NEEDS-ACTION'); +}); + +it('marks declined events when the feed owner attendee has declined', function () { + $feed = new CalendarFeed([ + 'name' => 'Work', + 'url' => 'https://calendar.example.com/basic.ics', + 'color' => '#2563eb', + ]); + $feed->id = 10; + $feed->setRelation('user', User::factory()->make([ + 'email' => 'andy@example.com', + 'timezone' => 'America/Chicago', + ])); + + $events = resolve(FetchCalendarEvents::class)->parse( + calendarFixture(<<first()['response_status'])->toBe('DECLINED'); +}); + function calendarFixture(string $events = ''): string { $events = $events ?: << ($event['response_status'] ?? null) === 'DECLINED', + ]) + style="border-left: 4px {{ ($event['response_status'] ?? null) === 'NEEDS-ACTION' ? 'dashed' : 'solid' }} {{ $event['feed_color'] }}" >
{{ $event['all_day'] ? __('All day') : $event['starts_at']->format('g:i A') }} From 9c68be08121ab2038e16da77a01baa98f239fb46 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 13:40:07 -0500 Subject: [PATCH 07/57] =?UTF-8?q?Just=20Keep=20Swimming=20=F0=9F=90=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Concerns/ProfileValidationRules.php | 7 - app/Livewire/Forms/Kiosk/SettingsForm.php | 4 +- database/factories/TeamFactory.php | 3 + database/factories/UserFactory.php | 6 + .../dashboard/\342\232\241calendar.blade.php" | 349 ------------------ .../dashboard/\342\232\241calendar.test.php" | 296 --------------- .../components/kiosk/calendar/event.blade.php | 18 + .../\342\232\241calendar/calendar.blade.php" | 36 +- .../\342\232\241calendar/calendar.test.php" | 16 +- .../chore-chart.test.php" | 17 +- .../kiosk/\342\232\241lists/lists.test.php" | 17 +- .../meal-planning.test.php" | 17 +- .../\342\232\241routines/routines.test.php" | 17 +- .../\342\232\241settings/settings.test.php" | 57 ++- .../settings/\342\232\241profile.blade.php" | 14 - tests/Feature/Settings/ProfileUpdateTest.php | 12 - 16 files changed, 149 insertions(+), 737 deletions(-) delete mode 100644 "resources/views/components/dashboard/\342\232\241calendar.blade.php" delete mode 100644 "resources/views/components/dashboard/\342\232\241calendar.test.php" create mode 100644 resources/views/components/kiosk/calendar/event.blade.php diff --git a/app/Concerns/ProfileValidationRules.php b/app/Concerns/ProfileValidationRules.php index c2c7e49..3005420 100644 --- a/app/Concerns/ProfileValidationRules.php +++ b/app/Concerns/ProfileValidationRules.php @@ -16,7 +16,6 @@ protected function profileRules(?int $userId = null): array return [ 'name' => $this->nameRules(), 'email' => $this->emailRules($userId), - 'timezone' => $this->timezoneRules(), ]; } @@ -39,10 +38,4 @@ protected function emailRules(?int $userId = null): array : Rule::unique(User::class)->ignore($userId), ]; } - - /** @return array|string> */ - protected function timezoneRules(): array - { - return ['required', 'string', Rule::in(timezone_identifiers_list())]; - } } diff --git a/app/Livewire/Forms/Kiosk/SettingsForm.php b/app/Livewire/Forms/Kiosk/SettingsForm.php index af97c5b..de0a812 100644 --- a/app/Livewire/Forms/Kiosk/SettingsForm.php +++ b/app/Livewire/Forms/Kiosk/SettingsForm.php @@ -11,10 +11,10 @@ class SettingsForm extends Form { public Team $editingTeam; - #[Validate('required')] + #[Validate('required|string|timezone:all')] public string $timezone = 'America/Chicago'; - #[Validate('required')] + #[Validate('required|int|between:0,6')] public int $week_start = Carbon::SUNDAY; public function load(Team $team) diff --git a/database/factories/TeamFactory.php b/database/factories/TeamFactory.php index 21ff380..db870a7 100644 --- a/database/factories/TeamFactory.php +++ b/database/factories/TeamFactory.php @@ -6,6 +6,7 @@ use App\Models\Team; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Carbon; use Illuminate\Support\Str; /** @@ -22,6 +23,8 @@ public function definition(): array 'name' => $name, 'slug' => Str::slug($name), 'is_personal' => false, + 'timezone' => 'America/Chicago', + 'week_start' => Carbon::SUNDAY, ]; } diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 00704a8..34ef8ee 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -63,4 +63,10 @@ public function withTwoFactor(): static 'two_factor_confirmed_at' => now(), ]); } + + public function memberOf(Team $team): static + { + return $this->hasAttached($team) + ->afterCreating(fn ($user) => $user->switchTeam($team)); + } } diff --git "a/resources/views/components/dashboard/\342\232\241calendar.blade.php" "b/resources/views/components/dashboard/\342\232\241calendar.blade.php" deleted file mode 100644 index ec199d5..0000000 --- "a/resources/views/components/dashboard/\342\232\241calendar.blade.php" +++ /dev/null @@ -1,349 +0,0 @@ - 'Blue', - '#16a34a' => 'Green', - '#dc2626' => 'Red', - '#9333ea' => 'Purple', - '#ea580c' => 'Orange', - '#0891b2' => 'Cyan', - '#ca8a04' => 'Gold', - '#4f46e5' => 'Indigo', - ]; - - public string $feedName = ''; - - public string $feedUrl = ''; - - public string $feedColor = self::DEFAULT_COLOR; - - public string $weekStartDate = ''; - - public function mount(): void - { - $this->weekStartDate = CarbonImmutable::now($this->timezoneName()) - ->startOfWeek(CarbonInterface::SUNDAY) - ->toDateString(); - } - - /** @return EloquentCollection */ - #[Computed] - public function feeds(): EloquentCollection - { - return Auth::user() - ->calendarFeeds() - ->orderBy('name') - ->get(); - } - - /** @return array> */ - #[Computed] - public function weekEvents(): array - { - $weekStartsAt = $this->weekStartsAt(); - - return $this->feeds - ->flatMap(function (CalendarFeed $feed) { - try { - return resolve(FetchCalendarEvents::class)->handle($feed, 7, $this->weekStartsAt()); - } catch (Throwable) { - return collect(); - } - }) - ->sortBy('starts_at') - ->take(12) - ->values() - ->all(); - } - - /** @return array>, is_today: bool}> */ - #[Computed] - public function weekDays(): array - { - $events = collect($this->weekEvents); - - return collect(range(0, 6)) - ->map(function (int $offset) use ($events): array { - $date = $this->weekStartsAt()->addDays($offset); - - return [ - 'date' => $date, - 'events' => $events - ->filter(fn (array $event) => $event['starts_at']->isSameDay($date)) - ->values() - ->all(), - 'is_today' => $date->isSameDay(CarbonImmutable::now($this->timezoneName())), - ]; - }) - ->all(); - } - - #[Computed] - public function weekLabel(): string - { - $weekStartsAt = $this->weekStartsAt(); - $weekEndsAt = $weekStartsAt->addDays(6); - - if ($weekStartsAt->isSameMonth($weekEndsAt)) { - return $weekStartsAt->format('M j') . ' - ' . $weekEndsAt->format('j, Y'); - } - - return $weekStartsAt->format('M j') . ' - ' . $weekEndsAt->format('M j, Y'); - } - - /** @return array */ - public function colorOptions(): array - { - return self::COLOR_OPTIONS; - } - - public function addFeed(): void - { - $this->feedUrl = rtrim($this->feedUrl, " \t\n\r\0\x0B,"); - - $validated = $this->validate([ - 'feedName' => ['nullable', 'string', 'max:255'], - 'feedUrl' => ['required', 'url', 'starts_with:http://,https://', 'max:2048'], - 'feedColor' => ['required', Rule::in(array_keys(self::COLOR_OPTIONS))], - ]); - - $url = $validated['feedUrl']; - $host = parse_url($url, PHP_URL_HOST); - - Auth::user()->calendarFeeds()->create([ - 'name' => filled($validated['feedName']) ? $validated['feedName'] : Str::headline((string) $host), - 'url' => $url, - 'color' => $validated['feedColor'], - ]); - - $this->reset('feedName', 'feedUrl'); - $this->feedColor = self::DEFAULT_COLOR; - unset($this->feeds, $this->weekEvents, $this->weekDays); - - Flux::toast(__('Calendar feed added.')); - } - - public function updateFeedColor(int $feedId, string $color): void - { - validator(['color' => $color], [ - 'color' => [Rule::in(array_keys(self::COLOR_OPTIONS))], - ])->validate(); - - Auth::user()->calendarFeeds()->whereKey($feedId)->update(['color' => $color]); - - unset($this->feeds, $this->weekEvents, $this->weekDays); - } - - public function deleteFeed(int $feedId): void - { - Auth::user()->calendarFeeds()->whereKey($feedId)->delete(); - - unset($this->feeds, $this->weekEvents, $this->weekDays); - - Flux::toast(__('Calendar feed removed.')); - } - - public function previousWeek(): void - { - $this->weekStartDate = $this->weekStartsAt()->subWeek()->toDateString(); - unset($this->weekEvents, $this->weekDays, $this->weekLabel); - } - - public function nextWeek(): void - { - $this->weekStartDate = $this->weekStartsAt()->addWeek()->toDateString(); - unset($this->weekEvents, $this->weekDays, $this->weekLabel); - } - - public function currentWeek(): void - { - $this->weekStartDate = CarbonImmutable::now($this->timezoneName()) - ->startOfWeek(CarbonInterface::SUNDAY) - ->toDateString(); - - unset($this->weekEvents, $this->weekDays, $this->weekLabel); - } - - private function weekStartsAt(): CarbonImmutable - { - return CarbonImmutable::parse($this->weekStartDate, $this->timezoneName()) - ->startOfWeek(CarbonInterface::SUNDAY); - } - - private function timezoneName(): string - { - return Auth::user()->timezone ?: 'America/Chicago'; - } -}; -?> - -
-
-
- {{ __('Calendar') }} - - {{ __('Weekly events from your subscribed calendars') }} - -
- -
- - -
- - {{ __('Add') }} - -
-
- {{ __('Color') }} -
- @foreach ($this->colorOptions() as $color => $label) - - @endforeach -
- -
- -
- -
-
-
-
- {{ __('Week') }} - {{ $this->weekLabel }} -
- -
- - {{ __('Today') }} - -
-
- - @if ($this->feeds->isEmpty()) -
- - - {{ __('Add a calendar feed to see weekly events.') }} - -
- @else -
- @foreach ($this->weekDays as $day) -
-
! $day['is_today'], - 'bg-blue-100 dark:bg-blue-900' => $day['is_today'], - ])> -
-
{{ $day['date']->format('D') }}
-
{{ $day['date']->format('M j') }}
-
-
- -
- @forelse ($day['events'] as $event) -
-
- - {{ $event['all_day'] ? __('All day') : $event['starts_at']->format('g:i A') }} -
- -
{{ $event['title'] }}
-
{{ $event['feed_name'] }}
- - @if ($event['location']) -
{{ $event['location'] }}
- @endif -
- @empty - @endforelse -
-
- @endforeach -
- @endif -
- -
-
- {{ __('Feeds') }} -
- -
- @forelse ($this->feeds as $feed) -
-
-
- -
{{ $feed->name }}
-
-
{{ parse_url($feed->url, PHP_URL_HOST) }}
-
- @foreach ($this->colorOptions() as $color => $label) - - - - @endforeach -
-
- - - - -
- @empty -
- {{ __('No feeds yet') }} -
- @endforelse -
-
-
-
diff --git "a/resources/views/components/dashboard/\342\232\241calendar.test.php" "b/resources/views/components/dashboard/\342\232\241calendar.test.php" deleted file mode 100644 index d7e79b4..0000000 --- "a/resources/views/components/dashboard/\342\232\241calendar.test.php" +++ /dev/null @@ -1,296 +0,0 @@ -create()) - ->test('dashboard.calendar') - ->assertStatus(200); -}); - -it('adds calendar feeds for the authenticated user', function () { - $user = User::factory()->create(); - - Livewire::actingAs($user) - ->test('dashboard.calendar') - ->set('feedName', 'Work') - ->set('feedUrl', 'https://calendar.example.com/basic.ics,') - ->set('feedColor', '#16a34a') - ->call('addFeed') - ->assertSet('feedName', '') - ->assertSet('feedUrl', '') - ->assertSet('feedColor', '#2563eb'); - - expect($user->calendarFeeds()->first()) - ->name->toBe('Work') - ->url->toBe('https://calendar.example.com/basic.ics') - ->color->toBe('#16a34a'); -}); - -it('requires a valid calendar feed url', function () { - Livewire::actingAs(User::factory()->create()) - ->test('dashboard.calendar') - ->set('feedUrl', 'not-a-url') - ->call('addFeed') - ->assertHasErrors(['feedUrl']); -}); - -it('requires a valid calendar feed color', function () { - Livewire::actingAs(User::factory()->create()) - ->test('dashboard.calendar') - ->set('feedUrl', 'https://calendar.example.com/basic.ics') - ->set('feedColor', '#ffffff') - ->call('addFeed') - ->assertHasErrors(['feedColor']); -}); - -it('updates only the authenticated users feed colors', function () { - $user = User::factory()->create(); - $otherUser = User::factory()->create(); - $feed = CalendarFeed::factory()->for($user)->create(['color' => '#2563eb']); - $otherFeed = CalendarFeed::factory()->for($otherUser)->create(['color' => '#2563eb']); - - Livewire::actingAs($user) - ->test('dashboard.calendar') - ->call('updateFeedColor', $feed->id, '#dc2626') - ->call('updateFeedColor', $otherFeed->id, '#dc2626'); - - expect($feed->fresh()->color)->toBe('#dc2626') - ->and($otherFeed->fresh()->color)->toBe('#2563eb'); -}); - -it('removes only the authenticated users calendar feeds', function () { - $user = User::factory()->create(); - $otherUser = User::factory()->create(); - $feed = CalendarFeed::factory()->for($user)->create(); - $otherFeed = CalendarFeed::factory()->for($otherUser)->create(); - - Livewire::actingAs($user) - ->test('dashboard.calendar') - ->call('deleteFeed', $feed->id) - ->call('deleteFeed', $otherFeed->id); - - expect($feed->fresh())->toBeNull() - ->and($otherFeed->fresh())->not->toBeNull(); -}); - -it('shows upcoming events from calendar feeds', function () { - CarbonImmutable::setTestNow('2026-05-14 09:00:00'); - Cache::store()->flush(); - Http::fake([ - 'calendar.example.com/*' => Http::response(calendarFixture()), - ]); - - $user = User::factory()->create(); - CalendarFeed::factory()->for($user)->create([ - 'name' => 'Work', - 'url' => 'https://calendar.example.com/basic.ics', - 'color' => '#9333ea', - ]); - - Livewire::actingAs($user) - ->test('dashboard.calendar') - ->assertSee('Design review') - ->assertSee('Work') - ->assertSee('#9333ea') - ->assertSee('10:00 AM') - ->assertSee('Office'); - - CarbonImmutable::setTestNow(); -}); - -it('shows events for the selected week and can navigate weeks', function () { - CarbonImmutable::setTestNow('2026-05-14 09:00:00'); - Cache::store()->flush(); - Http::fake([ - 'calendar.example.com/*' => Http::response(calendarFixture(<<create(); - CalendarFeed::factory()->for($user)->create([ - 'name' => 'Work', - 'url' => 'https://calendar.example.com/basic.ics', - ]); - - Livewire::actingAs($user) - ->test('dashboard.calendar') - ->assertSee('May 10 - 16, 2026') - ->assertSee('This week review') - ->assertDontSee('Next week planning') - ->call('nextWeek') - ->assertSee('May 17 - 23, 2026') - ->assertSee('Next week planning') - ->assertDontSee('This week review'); - - CarbonImmutable::setTestNow(); -}); - -it('expands simple recurring calendar events', function () { - $feed = CalendarFeed::factory()->make([ - 'id' => 10, - 'name' => 'Family', - ]); - - $events = resolve(FetchCalendarEvents::class)->parse( - calendarFixture(<<toHaveCount(3) - ->and($events->pluck('title')->all())->toBe(['Standup', 'Standup', 'Standup']); -}); - -it('honors excluded recurring calendar dates', function () { - $feed = CalendarFeed::factory()->make([ - 'id' => 10, - 'name' => 'Family', - ]); - - $events = resolve(FetchCalendarEvents::class)->parse( - calendarFixture(<<toHaveCount(2) - ->and($events->pluck('starts_at')->map->toDateString()->all())->toBe(['2026-05-14', '2026-05-28']); -}); - -it('converts calendar feed times to the users timezone', function () { - $user = User::factory()->create(['timezone' => 'America/New_York']); - $feed = CalendarFeed::factory()->for($user)->create([ - 'name' => 'Work', - 'url' => 'https://calendar.example.com/basic.ics', - ]); - - $events = resolve(FetchCalendarEvents::class)->parse( - calendarFixture(), - $feed, - CarbonImmutable::parse('2026-05-14 00:00:00', 'America/New_York'), - 30, - ); - - expect($events)->toHaveCount(1) - ->and($events->first()['starts_at']->timezoneName)->toBe('America/New_York') - ->and($events->first()['starts_at']->format('g:i A'))->toBe('11:00 AM'); -}); - -it('marks events pending response when the feed owner attendee needs action', function () { - $feed = new CalendarFeed([ - 'name' => 'Work', - 'url' => 'https://calendar.example.com/basic.ics', - 'color' => '#2563eb', - ]); - $feed->id = 10; - $feed->setRelation('user', User::factory()->make([ - 'email' => 'andy@example.com', - 'timezone' => 'America/Chicago', - ])); - - $events = resolve(FetchCalendarEvents::class)->parse( - calendarFixture(<<first()['response_status'])->toBe('NEEDS-ACTION'); -}); - -it('marks declined events when the feed owner attendee has declined', function () { - $feed = new CalendarFeed([ - 'name' => 'Work', - 'url' => 'https://calendar.example.com/basic.ics', - 'color' => '#2563eb', - ]); - $feed->id = 10; - $feed->setRelation('user', User::factory()->make([ - 'email' => 'andy@example.com', - 'timezone' => 'America/Chicago', - ])); - - $events = resolve(FetchCalendarEvents::class)->parse( - calendarFixture(<<first()['response_status'])->toBe('DECLINED'); -}); - -function calendarFixture(string $events = ''): string -{ - $events = $events ?: << ($event['response_status'] ?? null) === 'DECLINED', + ]) + style="border-left: 4px {{ ($event['response_status'] ?? null) === 'NEEDS-ACTION' ? 'dashed' : 'solid' }} {{ $event['feed_color'] }}" +> +
+ {{ $event['all_day'] ? __('All day') : $event['starts_at']->format('g:i A') }} +
+ +
{{ $event['title'] }}
+ + @if ($event['location']) +
{{ $event['location'] }}
+ @endif +
diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" index 9097173..642b9ab 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" @@ -33,7 +33,7 @@
-
+
@foreach ($this->weekDays as $day)
$day['is_today'], ])>
-
{{ $day['date']->format('D') }}
-
{{ $day['date']->format('M j') }}
+
{{ $day['date']->format('D') }}
+
{{ $day['date']->format('M j') }}
- @forelse ($day['events'] as $event) -
($event['response_status'] ?? null) === 'DECLINED', - ]) - style="border-left: 4px {{ ($event['response_status'] ?? null) === 'NEEDS-ACTION' ? 'dashed' : 'solid' }} {{ $event['feed_color'] }}" - > -
- {{ $event['all_day'] ? __('All day') : $event['starts_at']->format('g:i A') }} -
- -
{{ $event['title'] }}
- - @if ($event['location']) -
{{ $event['location'] }}
- @endif -
- @empty - @endforelse + @foreach ($day['events'] as $event) + + @endforeach
@endforeach diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" index 5cf9d82..a46e94a 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" @@ -3,8 +3,16 @@ use App\Models\User; use Livewire\Livewire; -it('renders successfully', function () { - Livewire::actingAs(User::factory()->create()) +use function Pest\Laravel\actingAs; + +test('renders successfully', function () { + $user = User::factory()->create(); + + actingAs($user) + ->get(route('kiosk.calendar')) + ->assertOk(); + + Livewire::actingAs($user) ->test('pages::kiosk.calendar') - ->assertStatus(200); -}); + ->assertOk(); +})->group('smoke'); diff --git "a/resources/views/pages/kiosk/\342\232\241chore-chart/chore-chart.test.php" "b/resources/views/pages/kiosk/\342\232\241chore-chart/chore-chart.test.php" index 8a69dfa..1707b74 100644 --- "a/resources/views/pages/kiosk/\342\232\241chore-chart/chore-chart.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241chore-chart/chore-chart.test.php" @@ -3,9 +3,16 @@ use App\Models\User; use Livewire\Livewire; -it('renders successfully', function () { - Livewire::actingAs(User::factory()->create()) +use function Pest\Laravel\actingAs; + +test('renders successfully', function () { + $user = User::factory()->create(); + + actingAs($user) + ->get(route('kiosk.chore-chart')) + ->assertOk(); + + Livewire::actingAs($user) ->test('pages::kiosk.chore-chart') - ->assertStatus(200) - ->assertSee('Chore Chart'); -}); + ->assertOk(); +})->group('smoke'); diff --git "a/resources/views/pages/kiosk/\342\232\241lists/lists.test.php" "b/resources/views/pages/kiosk/\342\232\241lists/lists.test.php" index 0c3671b..18106bc 100644 --- "a/resources/views/pages/kiosk/\342\232\241lists/lists.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241lists/lists.test.php" @@ -3,9 +3,16 @@ use App\Models\User; use Livewire\Livewire; -it('renders successfully', function () { - Livewire::actingAs(User::factory()->create()) +use function Pest\Laravel\actingAs; + +test('renders successfully', function () { + $user = User::factory()->create(); + + actingAs($user) + ->get(route('kiosk.lists')) + ->assertOk(); + + Livewire::actingAs($user) ->test('pages::kiosk.lists') - ->assertStatus(200) - ->assertSee('Lists'); -}); + ->assertOk(); +})->group('smoke'); diff --git "a/resources/views/pages/kiosk/\342\232\241meal-planning/meal-planning.test.php" "b/resources/views/pages/kiosk/\342\232\241meal-planning/meal-planning.test.php" index b2a98bb..b8e8c3a 100644 --- "a/resources/views/pages/kiosk/\342\232\241meal-planning/meal-planning.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241meal-planning/meal-planning.test.php" @@ -3,9 +3,16 @@ use App\Models\User; use Livewire\Livewire; -it('renders successfully', function () { - Livewire::actingAs(User::factory()->create()) +use function Pest\Laravel\actingAs; + +test('renders successfully', function () { + $user = User::factory()->create(); + + actingAs($user) + ->get(route('kiosk.meal-planning')) + ->assertOk(); + + Livewire::actingAs($user) ->test('pages::kiosk.meal-planning') - ->assertStatus(200) - ->assertSee('Meal Planning'); -}); + ->assertOk(); +})->group('smoke'); diff --git "a/resources/views/pages/kiosk/\342\232\241routines/routines.test.php" "b/resources/views/pages/kiosk/\342\232\241routines/routines.test.php" index 3a5eb66..1b3dd3d 100644 --- "a/resources/views/pages/kiosk/\342\232\241routines/routines.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241routines/routines.test.php" @@ -3,9 +3,16 @@ use App\Models\User; use Livewire\Livewire; -it('renders successfully', function () { - Livewire::actingAs(User::factory()->create()) +use function Pest\Laravel\actingAs; + +test('renders successfully', function () { + $user = User::factory()->create(); + + actingAs($user) + ->get(route('kiosk.routines')) + ->assertOk(); + + Livewire::actingAs($user) ->test('pages::kiosk.routines') - ->assertStatus(200) - ->assertSee('Routines'); -}); + ->assertOk(); +})->group('smoke'); diff --git "a/resources/views/pages/kiosk/\342\232\241settings/settings.test.php" "b/resources/views/pages/kiosk/\342\232\241settings/settings.test.php" index 34b2aa5..70a156a 100644 --- "a/resources/views/pages/kiosk/\342\232\241settings/settings.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241settings/settings.test.php" @@ -1,8 +1,59 @@ assertStatus(200); +use function Pest\Laravel\actingAs; + +test('renders successfully', function () { + $team = Team::factory()->create([ + 'name' => 'Straw Hats', + 'timezone' => 'Asia/Tokyo', + 'week_start' => Carbon::MONDAY, + ]); + + $user = User::factory()->memberOf($team)->create(); + + actingAs($user) + ->get(route('kiosk.settings')) + ->assertOk(); + + Livewire::actingAs($user) + ->test('pages::kiosk.settings') + ->assertOk() + ->assertSet('form.timezone', 'Asia/Tokyo') + ->assertSet('form.week_start', Carbon::MONDAY); +})->group('smoke'); + +test('can change kiosk settings', function () { + $team = Team::factory()->create([ + 'name' => 'Straw Hats', + 'timezone' => 'Asia/Tokyo', + 'week_start' => Carbon::MONDAY, + ]); + + $user = User::factory()->memberOf($team)->create(); + + Livewire::actingAs($user) + ->test('pages::kiosk.settings') + ->set('form.timezone', 'America/Sao_Paulo') + ->set('form.week_start', Carbon::SUNDAY) + ->call('save'); + + expect($team->fresh()) + ->timezone->toBe('America/Sao_Paulo') + ->week_start->toBe(Carbon::SUNDAY); +}); + +test('options must be valid', function () { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test('pages::kiosk.settings') + ->set('form.timezone', 'Not/AZone') + ->set('form.week_start', 15) + ->call('save') + ->assertHasErrors(['form.timezone', 'form.week_start']); }); diff --git "a/resources/views/pages/settings/\342\232\241profile.blade.php" "b/resources/views/pages/settings/\342\232\241profile.blade.php" index f30d7f2..75c6cf2 100644 --- "a/resources/views/pages/settings/\342\232\241profile.blade.php" +++ "b/resources/views/pages/settings/\342\232\241profile.blade.php" @@ -14,13 +14,11 @@ public string $name = ''; public string $email = ''; - public string $timezone = 'America/Chicago'; public function mount(): void { $this->name = Auth::user()->name; $this->email = Auth::user()->email; - $this->timezone = Auth::user()->timezone; } public function updateProfileInformation(): void @@ -67,12 +65,6 @@ public function showDeleteUser(): bool return ! Auth::user() instanceof MustVerifyEmail || (Auth::user() instanceof MustVerifyEmail && Auth::user()->hasVerifiedEmail()); } - - /** @return array */ - public function timezones(): array - { - return \DateTimeZone::listIdentifiers(); - } }; ?>
@@ -106,12 +98,6 @@ public function timezones(): array @endif
- - @foreach ($this->timezones() as $timezoneOption) - {{ str_replace('_', ' ', $timezoneOption) }} - @endforeach - -
diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php index b5eec28..d5f703a 100644 --- a/tests/Feature/Settings/ProfileUpdateTest.php +++ b/tests/Feature/Settings/ProfileUpdateTest.php @@ -16,27 +16,15 @@ ->test('pages::settings.profile') ->set('name', 'Test User') ->set('email', 'test@example.com') - ->set('timezone', 'America/New_York') ->call('updateProfileInformation') ->assertHasNoErrors(); expect($user->fresh()) ->name->toEqual('Test User') ->email->toEqual('test@example.com') - ->timezone->toEqual('America/New_York') ->email_verified_at->toBeNull(); }); -test('profile timezone must be valid', function () { - $user = User::factory()->create(); - - Livewire::actingAs($user) - ->test('pages::settings.profile') - ->set('timezone', 'Not/AZone') - ->call('updateProfileInformation') - ->assertHasErrors(['timezone']); -}); - test('email verification status is unchanged when email address is unchanged', function () { $user = User::factory()->create(); From 1418ff50592dbc94246a3b49d81e1fcb00be9a8f Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 14:02:04 -0500 Subject: [PATCH 08/57] Remove calendar component from dashboard --- resources/views/dashboard.blade.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index c95cae6..c8dd8d5 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -1,3 +1,6 @@ - + What would you like to see here? + + {{ __('Make a Suggestion') }} + From 86a111dca3995b7cacd8ce34d9a21364cb63993d Mon Sep 17 00:00:00 2001 From: techenby <6541180+techenby@users.noreply.github.com> Date: Fri, 15 May 2026 19:02:46 +0000 Subject: [PATCH 09/57] Rector fixes --- app/Enums/CalendarColor.php | 2 ++ app/Livewire/Forms/Kiosk/SettingsForm.php | 2 ++ "resources/views/pages/kiosk/\342\232\241calendar/calendar.php" | 1 - 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Enums/CalendarColor.php b/app/Enums/CalendarColor.php index 9184c0b..8ef9c82 100644 --- a/app/Enums/CalendarColor.php +++ b/app/Enums/CalendarColor.php @@ -1,5 +1,7 @@ Date: Fri, 15 May 2026 19:03:12 +0000 Subject: [PATCH 10/57] Dusting --- .../components/kiosk/\342\232\241weather-tile/weather-tile.php" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" index 008929e..f1acb2f 100644 --- "a/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" +++ "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" @@ -5,4 +5,4 @@ new class extends Component { // -}; \ No newline at end of file +}; From 2da9c5a01cd6340de8d43d086e24b1c5b162de0f Mon Sep 17 00:00:00 2001 From: techenby <6541180+techenby@users.noreply.github.com> Date: Fri, 15 May 2026 19:03:14 +0000 Subject: [PATCH 11/57] Ignore Dusting commit in git blame --- .git-blame-ignore-revs | 1 + 1 file changed, 1 insertion(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index cde1494..03cd634 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1 +1,2 @@ 24b8f32bcf7e24f999bd8763bc9baca607fad516 +62f533191a8f08fb724b207b4e3f53f9ab2b35bb From f28390fdcd5652f490cc3b0c53eff312ad05ceb6 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 15:14:02 -0500 Subject: [PATCH 12/57] Add tests, move calendar feeds to teams --- app/Actions/Calendars/FetchCalendarEvents.php | 20 +++- app/Models/CalendarFeed.php | 8 +- app/Models/Team.php | 6 ++ app/Models/User.php | 8 -- database/factories/CalendarFeedFactory.php | 7 +- ..._14_204921_create_calendar_feeds_table.php | 4 +- .../\342\232\241calendar/calendar.blade.php" | 61 ++++++------- .../kiosk/\342\232\241calendar/calendar.php" | 17 ++-- .../\342\232\241calendar/calendar.test.php" | 91 +++++++++++++++++++ .../Unit/Actions/FetchCalendarEventsTest.php | 49 ++++++++++ 10 files changed, 204 insertions(+), 67 deletions(-) create mode 100644 tests/Unit/Actions/FetchCalendarEventsTest.php diff --git a/app/Actions/Calendars/FetchCalendarEvents.php b/app/Actions/Calendars/FetchCalendarEvents.php index ab4cedc..de54bf5 100644 --- a/app/Actions/Calendars/FetchCalendarEvents.php +++ b/app/Actions/Calendars/FetchCalendarEvents.php @@ -127,16 +127,16 @@ private function responseStatus(VEvent $event, CalendarFeed $feed): ?string return null; } - $email = mb_strtolower((string) $feed->user?->email); + $emails = $this->teamMemberEmails($feed); - if ($email === '') { + if ($emails->isEmpty()) { return null; } foreach ($event->ATTENDEE as $attendee) { $attendeeEmail = mb_strtolower(preg_replace('/^mailto:/i', '', (string) $attendee->getValue())); - if ($attendeeEmail !== $email) { + if (! $emails->contains($attendeeEmail)) { continue; } @@ -148,8 +148,20 @@ private function responseStatus(VEvent $event, CalendarFeed $feed): ?string return null; } + /** @return Collection */ + private function teamMemberEmails(CalendarFeed $feed): Collection + { + $feed->loadMissing('team.members'); + + return $feed->team->members + ->pluck('email') + ->map(fn (string $email): string => mb_strtolower($email)) + ->filter() + ->values(); + } + private function timezoneName(CalendarFeed $feed): string { - return $feed->user?->timezone ?: 'America/Chicago'; + return $feed->team?->timezone ?: 'America/Chicago'; } } diff --git a/app/Models/CalendarFeed.php b/app/Models/CalendarFeed.php index 0cef1d9..a1100ae 100644 --- a/app/Models/CalendarFeed.php +++ b/app/Models/CalendarFeed.php @@ -10,15 +10,15 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -#[Fillable(['user_id', 'name', 'url', 'color'])] +#[Fillable(['team_id', 'name', 'url', 'color'])] class CalendarFeed extends Model { /** @use HasFactory */ use HasFactory; - /** @return BelongsTo */ - public function user(): BelongsTo + /** @return BelongsTo */ + public function team(): BelongsTo { - return $this->belongsTo(User::class); + return $this->belongsTo(Team::class); } } diff --git a/app/Models/Team.php b/app/Models/Team.php index a2ca519..57194e2 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -38,6 +38,12 @@ protected static function boot(): void }); } + /** @return HasMany */ + public function calendarFeeds(): HasMany + { + return $this->hasMany(CalendarFeed::class); + } + public function owner(): ?Model { return $this->members() diff --git a/app/Models/User.php b/app/Models/User.php index ff9afc5..a1862e0 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,7 +10,6 @@ use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Factories\HasFactory; -use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Str; @@ -43,17 +42,10 @@ public function initials(): string public function purge(): void { $this->ownedTeams->each->purge(); - $this->calendarFeeds()->delete(); $this->teamMemberships()->delete(); $this->delete(); } - /** @return HasMany */ - public function calendarFeeds(): HasMany - { - return $this->hasMany(CalendarFeed::class); - } - /** @return array */ protected function casts(): array { diff --git a/database/factories/CalendarFeedFactory.php b/database/factories/CalendarFeedFactory.php index 4023ce7..1351e73 100644 --- a/database/factories/CalendarFeedFactory.php +++ b/database/factories/CalendarFeedFactory.php @@ -4,8 +4,9 @@ namespace Database\Factories; +use App\Enums\CalendarColor; use App\Models\CalendarFeed; -use App\Models\User; +use App\Models\Team; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -17,10 +18,10 @@ class CalendarFeedFactory extends Factory public function definition(): array { return [ - 'user_id' => User::factory(), + 'team_id' => Team::factory(), 'name' => fake()->words(2, true), 'url' => fake()->url() . '/calendar.ics', - 'color' => fake()->randomElement(['#2563eb', '#16a34a', '#dc2626', '#9333ea', '#ea580c', '#0891b2']), + 'color' => fake()->randomElement(CalendarColor::class), ]; } } diff --git a/database/migrations/2026_05_14_204921_create_calendar_feeds_table.php b/database/migrations/2026_05_14_204921_create_calendar_feeds_table.php index 361e609..4ecdbac 100644 --- a/database/migrations/2026_05_14_204921_create_calendar_feeds_table.php +++ b/database/migrations/2026_05_14_204921_create_calendar_feeds_table.php @@ -10,13 +10,13 @@ public function up(): void { Schema::create('calendar_feeds', function (Blueprint $table) { $table->id(); - $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('team_id')->constrained()->cascadeOnDelete(); $table->string('name'); $table->text('url'); $table->string('color', 7)->default('#2563eb'); $table->timestamps(); - $table->index(['user_id', 'name']); + $table->index(['team_id', 'name']); }); } diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" index 642b9ab..4ff9789 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" @@ -17,43 +17,34 @@
- @if ($this->feeds->isEmpty()) -
- - - {{ __('Add a calendar feed to see weekly events.') }} - -
- @else -
- - @foreach ($this->feeds as $feed) - - @endforeach - -
+
+ + @foreach ($this->feeds as $feed) + + @endforeach + +
-
- @foreach ($this->weekDays as $day) -
-
! $day['is_today'], - 'bg-blue-100 dark:bg-blue-900' => $day['is_today'], - ])> -
-
{{ $day['date']->format('D') }}
-
{{ $day['date']->format('M j') }}
-
+
+ @foreach ($this->weekDays as $day) +
+
! $day['is_today'], + 'bg-blue-100 dark:bg-blue-900' => $day['is_today'], + ])> +
+
{{ $day['date']->format('D') }}
+
{{ $day['date']->format('M j') }}
+
-
- @foreach ($day['events'] as $event) - - @endforeach -
+
+ @foreach ($day['events'] as $event) + + @endforeach
- @endforeach -
- @endif +
+ @endforeach +
diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" index 3edd10e..ff8281a 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" @@ -3,7 +3,6 @@ use App\Actions\Calendars\FetchCalendarEvents; use App\Models\CalendarFeed; use Carbon\CarbonImmutable; -use Carbon\CarbonInterface; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Support\Facades\Auth; use Livewire\Attributes\Computed; @@ -31,7 +30,7 @@ public function mount(): void #[Computed] public function feeds(): EloquentCollection { - return Auth::user() + return Auth::user()->currentTeam ->calendarFeeds() ->get(); } @@ -43,11 +42,7 @@ public function weekEvents(): array return $this->feeds ->whereIn('id', $this->selectedFeeds) ->flatMap(function (CalendarFeed $feed) { - try { - return resolve(FetchCalendarEvents::class)->handle($feed, 7, $this->weekStartsAt()); - } catch (Throwable) { - return collect(); - } + return resolve(FetchCalendarEvents::class)->handle($feed, 7, $this->weekStartsAt()); }) ->sortBy('starts_at') ->values() @@ -85,22 +80,22 @@ public function nowLabel(): string public function previousWeek(): void { $this->weekStartDate = $this->weekStartsAt()->subWeek()->toDateString(); - unset($this->weekEvents, $this->weekDays, $this->weekLabel); + unset($this->weekEvents, $this->weekDays); } public function nextWeek(): void { $this->weekStartDate = $this->weekStartsAt()->addWeek()->toDateString(); - unset($this->weekEvents, $this->weekDays, $this->weekLabel); + unset($this->weekEvents, $this->weekDays); } public function currentWeek(): void { $this->weekStartDate = CarbonImmutable::now($this->timezoneName()) - ->startOfWeek(CarbonInterface::SUNDAY) + ->startOfWeek(Auth::user()->currentTeam->week_start) ->toDateString(); - unset($this->weekEvents, $this->weekDays, $this->weekLabel); + unset($this->weekEvents, $this->weekDays); } private function weekStartsAt(): CarbonImmutable diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" index a46e94a..a5824f3 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" @@ -1,6 +1,11 @@ test('pages::kiosk.calendar') ->assertOk(); })->group('smoke'); + +test('can view events from feed', function () { + Http::allowStrayRequests(['https://calendar.google.com/calendar/ical/*']); + + $this->travelTo(Carbon::parse('2026-05-08')); + + $team = Team::factory() + ->has(CalendarFeed::factory()->state([ + 'url' => 'https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics', + 'name' => 'US Holidays', + 'color' => CalendarColor::Green, + ])) + ->create(); + $user = User::factory()->memberOf($team)->create(); + + Livewire::actingAs($user) + ->test('pages::kiosk.calendar') + ->assertSee('Cinco de Mayo'); +}); + +test('can go to the next and previous weeks', function () { + $this->travelTo(Carbon::parse('2026-05-08')); + + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test('pages::kiosk.calendar') + ->assertSeeInOrder([ + 'calendar-day-2026-05-03', 'calendar-day-2026-05-04', 'calendar-day-2026-05-05', 'calendar-day-2026-05-06', 'calendar-day-2026-05-07', 'calendar-day-2026-05-08', 'calendar-day-2026-05-09', + ]) + ->call('previousWeek') + ->assertSeeInOrder([ + 'calendar-day-2026-04-26', 'calendar-day-2026-04-27', 'calendar-day-2026-04-28', 'calendar-day-2026-04-29', 'calendar-day-2026-04-30', 'calendar-day-2026-05-01', 'calendar-day-2026-05-02', + ]) + ->call('currentWeek') + ->assertSeeInOrder([ + 'calendar-day-2026-05-03', 'calendar-day-2026-05-04', 'calendar-day-2026-05-05', 'calendar-day-2026-05-06', 'calendar-day-2026-05-07', 'calendar-day-2026-05-08', 'calendar-day-2026-05-09', + ]) + ->call('nextWeek') + ->assertSeeInOrder([ + 'calendar-day-2026-05-10', 'calendar-day-2026-05-11', 'calendar-day-2026-05-12', 'calendar-day-2026-05-13', 'calendar-day-2026-05-14', 'calendar-day-2026-05-15', 'calendar-day-2026-05-16', + ]); +}); + +test('can hide feed from calendar', function () { + Http::allowStrayRequests([ + 'https://calendar.google.com/calendar/ical/*', + 'https://worldpublicholiday.com/calendar-feeds/*' + ]); + + $this->travelTo(Carbon::parse('2026-03-20')); + + $team = Team::factory() + ->has( + CalendarFeed::factory() + ->count(2) + ->sequence([ + 'url' => 'https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics', + 'name' => 'US Holidays', + 'color' => CalendarColor::Green, + ], [ + 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=BR&year=2026', + 'name' => 'Brazilian Holidays', + 'color' => CalendarColor::Blue, + ]) + ) + ->create(); + $user = User::factory()->memberOf($team)->create(); + [$brazilianHolidays, $usHolidays] = $team->calendarFeeds; + + Livewire::actingAs($user) + ->test('pages::kiosk.calendar') + ->assertSet('selectedFeeds', [$brazilianHolidays->id, $usHolidays->id]) + ->assertSeeInOrder([ + 'calendar-day-2026-03-17', "St. Patrick's Day", + 'calendar-day-2026-03-18', 'Autonomia do Estado', + 'calendar-day-2026-03-19', 'Dia de Sรฃo Josรฉ' + ]) + ->set('selectedFeeds', [$usHolidays->id]) + ->assertSeeInOrder([ + 'calendar-day-2026-03-17', "St. Patrick's Day", + 'calendar-day-2026-03-18', // 'Autonomia do Estado', + 'calendar-day-2026-03-19', // 'Dia de Sรฃo Josรฉ' + ]) + ->assertDontSee(['Autonomia do Estado', 'Dia de Sรฃo Josรฉ']); +}); diff --git a/tests/Unit/Actions/FetchCalendarEventsTest.php b/tests/Unit/Actions/FetchCalendarEventsTest.php new file mode 100644 index 0000000..425b26e --- /dev/null +++ b/tests/Unit/Actions/FetchCalendarEventsTest.php @@ -0,0 +1,49 @@ +create(); + $team->members()->attach( + User::factory()->create(['email' => 'zoro@example.com']), + ['role' => TeamRole::Member->value], + ); + $team->members()->attach( + User::factory()->create(['email' => 'nami@example.com']), + ['role' => TeamRole::Member->value], + ); + + $feed = CalendarFeed::factory()->for($team)->create([ + 'name' => 'Crew Calendar', + 'url' => 'https://example.com/calendar.ics', + ]); + + $events = app(FetchCalendarEvents::class)->parse( + ics: <<<'ICS' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sunny//Calendar Test//EN +BEGIN:VEVENT +UID:crew-meeting@example.com +DTSTAMP:20260501T120000Z +DTSTART:20260505T150000Z +DTEND:20260505T160000Z +SUMMARY:Crew Meeting +ATTENDEE;PARTSTAT=ACCEPTED:mailto:zoro@example.com +ATTENDEE;PARTSTAT=DECLINED:mailto:sanji@example.com +END:VEVENT +END:VCALENDAR +ICS, + feed: $feed, + from: CarbonImmutable::parse('2026-05-05', 'America/Chicago'), + days: 1, + ); + + expect($events)->toHaveCount(1) + ->and($events->first()['response_status'])->toBe('ACCEPTED'); +}); From b243c02a26e225559deb1d1300853ce58b0444e4 Mon Sep 17 00:00:00 2001 From: techenby <6541180+techenby@users.noreply.github.com> Date: Fri, 15 May 2026 20:14:50 +0000 Subject: [PATCH 13/57] Rector fixes --- app/Actions/Calendars/FetchCalendarEvents.php | 2 +- .../pages/kiosk/\342\232\241calendar/calendar.test.php" | 8 ++++---- tests/Unit/Actions/FetchCalendarEventsTest.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Actions/Calendars/FetchCalendarEvents.php b/app/Actions/Calendars/FetchCalendarEvents.php index de54bf5..1e2550e 100644 --- a/app/Actions/Calendars/FetchCalendarEvents.php +++ b/app/Actions/Calendars/FetchCalendarEvents.php @@ -136,7 +136,7 @@ private function responseStatus(VEvent $event, CalendarFeed $feed): ?string foreach ($event->ATTENDEE as $attendee) { $attendeeEmail = mb_strtolower(preg_replace('/^mailto:/i', '', (string) $attendee->getValue())); - if (! $emails->contains($attendeeEmail)) { + if ($emails->doesntContain($attendeeEmail)) { continue; } diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" index a5824f3..dbdac53 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" @@ -1,10 +1,10 @@ travelTo(Carbon::parse('2026-05-08')); + $this->travelTo(Date::parse('2026-05-08')); $team = Team::factory() ->has(CalendarFeed::factory()->state([ @@ -42,7 +42,7 @@ }); test('can go to the next and previous weeks', function () { - $this->travelTo(Carbon::parse('2026-05-08')); + $this->travelTo(Date::parse('2026-05-08')); $user = User::factory()->create(); @@ -71,7 +71,7 @@ 'https://worldpublicholiday.com/calendar-feeds/*' ]); - $this->travelTo(Carbon::parse('2026-03-20')); + $this->travelTo(Date::parse('2026-03-20')); $team = Team::factory() ->has( diff --git a/tests/Unit/Actions/FetchCalendarEventsTest.php b/tests/Unit/Actions/FetchCalendarEventsTest.php index 425b26e..69bdb58 100644 --- a/tests/Unit/Actions/FetchCalendarEventsTest.php +++ b/tests/Unit/Actions/FetchCalendarEventsTest.php @@ -23,7 +23,7 @@ 'url' => 'https://example.com/calendar.ics', ]); - $events = app(FetchCalendarEvents::class)->parse( + $events = resolve(FetchCalendarEvents::class)->parse( ics: <<<'ICS' BEGIN:VCALENDAR VERSION:2.0 From 14c4d44be34f0edd3b7de4370e420f636859eef6 Mon Sep 17 00:00:00 2001 From: techenby <6541180+techenby@users.noreply.github.com> Date: Fri, 15 May 2026 20:15:12 +0000 Subject: [PATCH 14/57] Dusting --- .../pages/kiosk/\342\232\241calendar/calendar.test.php" | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" index dbdac53..c5c3aa1 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" @@ -1,10 +1,10 @@ travelTo(Date::parse('2026-03-20')); @@ -97,7 +97,7 @@ ->assertSeeInOrder([ 'calendar-day-2026-03-17', "St. Patrick's Day", 'calendar-day-2026-03-18', 'Autonomia do Estado', - 'calendar-day-2026-03-19', 'Dia de Sรฃo Josรฉ' + 'calendar-day-2026-03-19', 'Dia de Sรฃo Josรฉ', ]) ->set('selectedFeeds', [$usHolidays->id]) ->assertSeeInOrder([ From 48e5c6fc380d82400261f958c45e31960926dbe2 Mon Sep 17 00:00:00 2001 From: techenby <6541180+techenby@users.noreply.github.com> Date: Fri, 15 May 2026 20:15:14 +0000 Subject: [PATCH 15/57] Ignore Dusting commit in git blame --- .git-blame-ignore-revs | 1 + 1 file changed, 1 insertion(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 03cd634..be1d517 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,3 @@ 24b8f32bcf7e24f999bd8763bc9baca607fad516 62f533191a8f08fb724b207b4e3f53f9ab2b35bb +e42a663c237250cc6e17eed50a7e3802d35e6a34 From d00233d46165d17803b559bf0169859107ecf49a Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 15:55:27 -0500 Subject: [PATCH 16/57] Move filters to dropdown --- .../\342\232\241calendar/calendar.blade.php" | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" index 4ff9789..1d40590 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" @@ -5,6 +5,20 @@
+ + + + + @foreach ($this->feeds as $feed) + + + {{ $feed->name }} + + @endforeach + + + + @@ -17,14 +31,6 @@
-
- - @foreach ($this->feeds as $feed) - - @endforeach - -
-
@foreach ($this->weekDays as $day)
From 79e1d64972c8b39f914c806664f464d3fe14ff1e Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 16:26:59 -0500 Subject: [PATCH 17/57] Add month view --- .../components/kiosk/calendar/month.blade.php | 53 +++++++++ .../components/kiosk/calendar/week.blade.php | 22 ++++ .../\342\232\241calendar/calendar.blade.php" | 33 ++---- .../kiosk/\342\232\241calendar/calendar.php" | 110 ++++++++++++++---- .../\342\232\241calendar/calendar.test.php" | 35 +++++- 5 files changed, 201 insertions(+), 52 deletions(-) create mode 100644 resources/views/components/kiosk/calendar/month.blade.php create mode 100644 resources/views/components/kiosk/calendar/week.blade.php diff --git a/resources/views/components/kiosk/calendar/month.blade.php b/resources/views/components/kiosk/calendar/month.blade.php new file mode 100644 index 0000000..7a9a3b5 --- /dev/null +++ b/resources/views/components/kiosk/calendar/month.blade.php @@ -0,0 +1,53 @@ +
+
+ @foreach (range(0, 6) as $offset) +
+ {{ $this->monthDays[$offset]['date']->format('D') }} +
+ @endforeach +
+ +
+ @foreach ($this->monthDays as $day) +
$day['is_current_month'] && ! $day['is_today'], + 'bg-zinc-50 text-zinc-500 dark:bg-zinc-900 dark:text-zinc-500' => ! $day['is_current_month'] && ! $day['is_today'], + 'bg-blue-100 dark:bg-blue-900' => $day['is_today'], + ]) + > +
$day['is_current_month'] || $day['is_today'], + 'text-zinc-400 dark:text-zinc-500' => ! $day['is_current_month'] && ! $day['is_today'], + ])> + {{ $day['date']->format('j') }} +
+ +
+ @foreach (array_slice($day['events'], 0, 3) as $event) +
($event['response_status'] ?? null) === 'DECLINED', + ]) + style="border-left: 3px {{ ($event['response_status'] ?? null) === 'NEEDS-ACTION' ? 'dashed' : 'solid' }} {{ $event['feed_color'] }}" + > + {{ $event['all_day'] ? __('All day') : $event['starts_at']->format('g:i A') }} + {{ $event['title'] }} +
+ @endforeach + + @if (count($day['events']) > 3) +
+ {{ __('+:count more', ['count' => count($day['events']) - 3]) }} +
+ @endif +
+
+ @endforeach +
+
diff --git a/resources/views/components/kiosk/calendar/week.blade.php b/resources/views/components/kiosk/calendar/week.blade.php new file mode 100644 index 0000000..2fae652 --- /dev/null +++ b/resources/views/components/kiosk/calendar/week.blade.php @@ -0,0 +1,22 @@ +
+ @foreach ($this->weekDays as $day) +
+
! $day['is_today'], + 'bg-blue-100 dark:bg-blue-900' => $day['is_today'], + ])> +
+
{{ $day['date']->format('D') }}
+
{{ $day['date']->format('M j') }}
+
+
+ +
+ @foreach ($day['events'] as $event) + + @endforeach +
+
+ @endforeach +
diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" index 1d40590..eef0573 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" @@ -25,32 +25,15 @@ - - {{ __('Today') }} - + + {{ __('Today') }} +
-
- @foreach ($this->weekDays as $day) -
-
! $day['is_today'], - 'bg-blue-100 dark:bg-blue-900' => $day['is_today'], - ])> -
-
{{ $day['date']->format('D') }}
-
{{ $day['date']->format('M j') }}
-
-
- -
- @foreach ($day['events'] as $event) - - @endforeach -
-
- @endforeach -
+ @if ($format === 'month') + + @else + + @endif
diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" index ff8281a..a6a0a96 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" @@ -11,7 +11,7 @@ new #[Layout('layouts::kiosk')] class extends Component { - public string $weekStartDate = ''; + public string $focusedDate = ''; public string $format = 'week'; @@ -19,9 +19,7 @@ public function mount(): void { - $this->weekStartDate = CarbonImmutable::now($this->timezoneName()) - ->startOfWeek(Auth::user()->currentTeam->week_start) - ->toDateString(); + $this->focusedDate = CarbonImmutable::now($this->timezoneName())->toDateString(); $this->selectedFeeds = $this->feeds->pluck('id')->toArray(); } @@ -39,14 +37,7 @@ public function feeds(): EloquentCollection #[Computed] public function weekEvents(): array { - return $this->feeds - ->whereIn('id', $this->selectedFeeds) - ->flatMap(function (CalendarFeed $feed) { - return resolve(FetchCalendarEvents::class)->handle($feed, 7, $this->weekStartsAt()); - }) - ->sortBy('starts_at') - ->values() - ->all(); + return $this->eventsForRange($this->weekStartsAt(), 7); } /** @return array>, is_today: bool}> */ @@ -71,39 +62,110 @@ public function weekDays(): array ->all(); } + /** @return array> */ + #[Computed] + public function monthEvents(): array + { + return $this->eventsForRange($this->monthGridStartsAt(), 42); + } + + /** @return array>, is_today: bool, is_current_month: bool}> */ + #[Computed] + public function monthDays(): array + { + $events = collect($this->monthEvents); + $month = $this->monthStartsAt(); + + return collect(range(0, 41)) + ->map(function (int $offset) use ($events, $month): array { + $date = $this->monthGridStartsAt()->addDays($offset); + + return [ + 'date' => $date, + 'events' => $events + ->filter(fn (array $event) => $event['starts_at']->isSameDay($date)) + ->values() + ->all(), + 'is_today' => $date->isSameDay(CarbonImmutable::now($this->timezoneName())), + 'is_current_month' => $date->isSameMonth($month), + ]; + }) + ->all(); + } + #[Computed] public function nowLabel(): string { return CarbonImmutable::now($this->timezoneName())->format('D, M j g:i A'); } - public function previousWeek(): void + public function previous(): void { - $this->weekStartDate = $this->weekStartsAt()->subWeek()->toDateString(); - unset($this->weekEvents, $this->weekDays); + $this->focusedDate = match ($this->format) { + 'month' => $this->focusedDate()->subMonthNoOverflow()->toDateString(), + default => $this->weekStartsAt()->subWeek()->toDateString(), + }; + + $this->clearCalendarState(); } - public function nextWeek(): void + public function next(): void { - $this->weekStartDate = $this->weekStartsAt()->addWeek()->toDateString(); - unset($this->weekEvents, $this->weekDays); + $this->focusedDate = match ($this->format) { + 'month' => $this->focusedDate()->addMonthNoOverflow()->toDateString(), + default => $this->weekStartsAt()->addWeek()->toDateString(), + }; + + $this->clearCalendarState(); } - public function currentWeek(): void + public function current(): void { - $this->weekStartDate = CarbonImmutable::now($this->timezoneName()) - ->startOfWeek(Auth::user()->currentTeam->week_start) - ->toDateString(); + $this->focusedDate = CarbonImmutable::now($this->timezoneName())->toDateString(); - unset($this->weekEvents, $this->weekDays); + $this->clearCalendarState(); } private function weekStartsAt(): CarbonImmutable { - return CarbonImmutable::parse($this->weekStartDate, $this->timezoneName()) + return $this->focusedDate() ->startOfWeek(Auth::user()->currentTeam->week_start); } + private function monthStartsAt(): CarbonImmutable + { + return $this->focusedDate()->startOfMonth(); + } + + private function monthGridStartsAt(): CarbonImmutable + { + return $this->monthStartsAt() + ->startOfWeek(Auth::user()->currentTeam->week_start); + } + + private function focusedDate(): CarbonImmutable + { + return CarbonImmutable::parse($this->focusedDate, $this->timezoneName()); + } + + /** @return array> */ + private function eventsForRange(CarbonImmutable $startsAt, int $days): array + { + return $this->feeds + ->whereIn('id', $this->selectedFeeds) + ->flatMap(function (CalendarFeed $feed) use ($days, $startsAt) { + return resolve(FetchCalendarEvents::class)->handle($feed, $days, $startsAt); + }) + ->sortBy('starts_at') + ->values() + ->all(); + } + + private function clearCalendarState(): void + { + unset($this->weekEvents, $this->weekDays, $this->monthEvents, $this->monthDays); + } + private function timezoneName(): string { return Auth::user()->currentTeam->timezone ?: 'America/Chicago'; diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" index c5c3aa1..5d95f55 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" @@ -38,6 +38,8 @@ Livewire::actingAs($user) ->test('pages::kiosk.calendar') + ->assertSee('Cinco de Mayo') + ->set('format', 'month') ->assertSee('Cinco de Mayo'); }); @@ -51,20 +53,47 @@ ->assertSeeInOrder([ 'calendar-day-2026-05-03', 'calendar-day-2026-05-04', 'calendar-day-2026-05-05', 'calendar-day-2026-05-06', 'calendar-day-2026-05-07', 'calendar-day-2026-05-08', 'calendar-day-2026-05-09', ]) - ->call('previousWeek') + ->call('previous') ->assertSeeInOrder([ 'calendar-day-2026-04-26', 'calendar-day-2026-04-27', 'calendar-day-2026-04-28', 'calendar-day-2026-04-29', 'calendar-day-2026-04-30', 'calendar-day-2026-05-01', 'calendar-day-2026-05-02', ]) - ->call('currentWeek') + ->call('current') ->assertSeeInOrder([ 'calendar-day-2026-05-03', 'calendar-day-2026-05-04', 'calendar-day-2026-05-05', 'calendar-day-2026-05-06', 'calendar-day-2026-05-07', 'calendar-day-2026-05-08', 'calendar-day-2026-05-09', ]) - ->call('nextWeek') + ->call('next') ->assertSeeInOrder([ 'calendar-day-2026-05-10', 'calendar-day-2026-05-11', 'calendar-day-2026-05-12', 'calendar-day-2026-05-13', 'calendar-day-2026-05-14', 'calendar-day-2026-05-15', 'calendar-day-2026-05-16', ]); }); +test('can view and navigate a month calendar', function () { + $this->travelTo(Date::parse('2026-05-15')); + + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test('pages::kiosk.calendar') + ->set('format', 'month') + ->assertSeeInOrder([ + 'calendar-day-2026-04-26', 'calendar-day-2026-04-27', 'calendar-day-2026-04-28', 'calendar-day-2026-04-29', 'calendar-day-2026-04-30', 'calendar-day-2026-05-01', 'calendar-day-2026-05-02', + 'calendar-day-2026-05-03', 'calendar-day-2026-05-04', 'calendar-day-2026-05-05', 'calendar-day-2026-05-06', 'calendar-day-2026-05-07', 'calendar-day-2026-05-08', 'calendar-day-2026-05-09', + 'calendar-day-2026-05-31', 'calendar-day-2026-06-01', 'calendar-day-2026-06-02', 'calendar-day-2026-06-03', 'calendar-day-2026-06-04', 'calendar-day-2026-06-05', 'calendar-day-2026-06-06', + ]) + ->call('previous') + ->assertSeeInOrder([ + 'calendar-day-2026-03-29', 'calendar-day-2026-03-30', 'calendar-day-2026-03-31', 'calendar-day-2026-04-01', 'calendar-day-2026-04-02', 'calendar-day-2026-04-03', 'calendar-day-2026-04-04', + ]) + ->call('current') + ->assertSeeInOrder([ + 'calendar-day-2026-04-26', 'calendar-day-2026-04-27', 'calendar-day-2026-04-28', 'calendar-day-2026-04-29', 'calendar-day-2026-04-30', 'calendar-day-2026-05-01', 'calendar-day-2026-05-02', + ]) + ->call('next') + ->assertSeeInOrder([ + 'calendar-day-2026-05-31', 'calendar-day-2026-06-01', 'calendar-day-2026-06-02', 'calendar-day-2026-06-03', 'calendar-day-2026-06-04', 'calendar-day-2026-06-05', 'calendar-day-2026-06-06', + ]); +}); + test('can hide feed from calendar', function () { Http::allowStrayRequests([ 'https://calendar.google.com/calendar/ical/*', From dfc30a4767248961ca8bf2a3262755319703b3ba Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 16:31:29 -0500 Subject: [PATCH 18/57] Add day view --- .../components/kiosk/calendar/day.blade.php | 27 +++++++++++++++++++ .../components/kiosk/calendar/week.blade.php | 2 +- .../\342\232\241calendar/calendar.blade.php" | 19 +++++++++---- .../kiosk/\342\232\241calendar/calendar.php" | 24 ++++++++++++++++- .../\342\232\241calendar/calendar.test.php" | 24 +++++++++++++++++ 5 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 resources/views/components/kiosk/calendar/day.blade.php diff --git a/resources/views/components/kiosk/calendar/day.blade.php b/resources/views/components/kiosk/calendar/day.blade.php new file mode 100644 index 0000000..52251e1 --- /dev/null +++ b/resources/views/components/kiosk/calendar/day.blade.php @@ -0,0 +1,27 @@ +
+
! $this->day['is_today'], + 'bg-blue-100 dark:bg-blue-900' => $this->day['is_today'], + ]) + > +
{{ $this->day['date']->format('l') }}
+
{{ $this->day['date']->format('F j') }}
+
+ +
+ @if (count($this->day['events']) > 0) +
+ @foreach ($this->day['events'] as $event) + + @endforeach +
+ @else +
+ {{ __('No events') }} +
+ @endif +
+
diff --git a/resources/views/components/kiosk/calendar/week.blade.php b/resources/views/components/kiosk/calendar/week.blade.php index 2fae652..6b300c0 100644 --- a/resources/views/components/kiosk/calendar/week.blade.php +++ b/resources/views/components/kiosk/calendar/week.blade.php @@ -2,7 +2,7 @@ @foreach ($this->weekDays as $day)
! $day['is_today'], 'bg-blue-100 dark:bg-blue-900' => $day['is_today'], ])> diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" index eef0573..0574d9e 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" @@ -31,9 +31,18 @@
- @if ($format === 'month') - - @else - - @endif + @switch($format) + @case('day') + + + @break + + @case('month') + + + @break + + @default + + @endswitch
diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" index a6a0a96..641a2ab 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" @@ -33,6 +33,26 @@ public function feeds(): EloquentCollection ->get(); } + /** @return array> */ + #[Computed] + public function dayEvents(): array + { + return $this->eventsForRange($this->focusedDate(), 1); + } + + /** @return array{date: CarbonImmutable, events: array>, is_today: bool} */ + #[Computed] + public function day(): array + { + $date = $this->focusedDate(); + + return [ + 'date' => $date, + 'events' => $this->dayEvents, + 'is_today' => $date->isSameDay(CarbonImmutable::now($this->timezoneName())), + ]; + } + /** @return array> */ #[Computed] public function weekEvents(): array @@ -102,6 +122,7 @@ public function nowLabel(): string public function previous(): void { $this->focusedDate = match ($this->format) { + 'day' => $this->focusedDate()->subDay()->toDateString(), 'month' => $this->focusedDate()->subMonthNoOverflow()->toDateString(), default => $this->weekStartsAt()->subWeek()->toDateString(), }; @@ -112,6 +133,7 @@ public function previous(): void public function next(): void { $this->focusedDate = match ($this->format) { + 'day' => $this->focusedDate()->addDay()->toDateString(), 'month' => $this->focusedDate()->addMonthNoOverflow()->toDateString(), default => $this->weekStartsAt()->addWeek()->toDateString(), }; @@ -163,7 +185,7 @@ private function eventsForRange(CarbonImmutable $startsAt, int $days): array private function clearCalendarState(): void { - unset($this->weekEvents, $this->weekDays, $this->monthEvents, $this->monthDays); + unset($this->dayEvents, $this->day, $this->weekEvents, $this->weekDays, $this->monthEvents, $this->monthDays); } private function timezoneName(): string diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" index 5d95f55..b5d7b23 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" @@ -39,10 +39,34 @@ Livewire::actingAs($user) ->test('pages::kiosk.calendar') ->assertSee('Cinco de Mayo') + ->set('format', 'day') + ->set('focusedDate', '2026-05-05') + ->assertSee('Cinco de Mayo') ->set('format', 'month') ->assertSee('Cinco de Mayo'); }); +test('can view and navigate a day calendar', function () { + $this->travelTo(Date::parse('2026-05-08 12:00:00', 'America/Chicago')); + + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test('pages::kiosk.calendar') + ->set('format', 'day') + ->assertSee('Friday') + ->assertSee('May 8') + ->call('previous') + ->assertSee('Thursday') + ->assertSee('May 7') + ->call('current') + ->assertSee('Friday') + ->assertSee('May 8') + ->call('next') + ->assertSee('Saturday') + ->assertSee('May 9'); +}); + test('can go to the next and previous weeks', function () { $this->travelTo(Date::parse('2026-05-08')); From eeca4a538e006cfe55b12398838597a306cd0856 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 16:44:52 -0500 Subject: [PATCH 19/57] Adjustments --- .../components/kiosk/calendar/day.blade.php | 54 ++++++----- .../components/kiosk/calendar/month.blade.php | 97 ++++++++++--------- .../\342\232\241calendar/calendar.blade.php" | 15 +-- 3 files changed, 82 insertions(+), 84 deletions(-) diff --git a/resources/views/components/kiosk/calendar/day.blade.php b/resources/views/components/kiosk/calendar/day.blade.php index 52251e1..ce237de 100644 --- a/resources/views/components/kiosk/calendar/day.blade.php +++ b/resources/views/components/kiosk/calendar/day.blade.php @@ -1,27 +1,33 @@ -
-
! $this->day['is_today'], - 'bg-blue-100 dark:bg-blue-900' => $this->day['is_today'], - ]) - > -
{{ $this->day['date']->format('l') }}
-
{{ $this->day['date']->format('F j') }}
-
+
+
+
! $this->day['is_today'], + 'bg-blue-100 dark:bg-blue-900' => $this->day['is_today'], + ]) + > +
{{ $this->day['date']->format('l') }}
+
{{ $this->day['date']->format('F j') }}
+
-
- @if (count($this->day['events']) > 0) -
- @foreach ($this->day['events'] as $event) - - @endforeach -
- @else -
- {{ __('No events') }} -
- @endif +
+ @if (count($this->day['events']) > 0) +
+ @foreach ($this->day['events'] as $event) + + @endforeach +
+ @endif +
+
+
+
diff --git a/resources/views/components/kiosk/calendar/month.blade.php b/resources/views/components/kiosk/calendar/month.blade.php index 7a9a3b5..b0df4d6 100644 --- a/resources/views/components/kiosk/calendar/month.blade.php +++ b/resources/views/components/kiosk/calendar/month.blade.php @@ -1,53 +1,58 @@ -
-
- @foreach (range(0, 6) as $offset) -
- {{ $this->monthDays[$offset]['date']->format('D') }} -
- @endforeach +
+
+ {{ now()->format('F') }}
- -
- @foreach ($this->monthDays as $day) -
$day['is_current_month'] && ! $day['is_today'], - 'bg-zinc-50 text-zinc-500 dark:bg-zinc-900 dark:text-zinc-500' => ! $day['is_current_month'] && ! $day['is_today'], - 'bg-blue-100 dark:bg-blue-900' => $day['is_today'], - ]) - > -
$day['is_current_month'] || $day['is_today'], - 'text-zinc-400 dark:text-zinc-500' => ! $day['is_current_month'] && ! $day['is_today'], - ])> - {{ $day['date']->format('j') }} +
+
+ @foreach (range(0, 6) as $offset) +
+ {{ $this->monthDays[$offset]['date']->format('D') }}
+ @endforeach +
+ +
+ @foreach ($this->monthDays as $day) +
$day['is_current_month'] && ! $day['is_today'], + 'bg-zinc-50 text-zinc-500 dark:bg-zinc-900 dark:text-zinc-500' => ! $day['is_current_month'] && ! $day['is_today'], + 'bg-blue-100 dark:bg-blue-900' => $day['is_today'], + ]) + > +
$day['is_current_month'] || $day['is_today'], + 'text-zinc-400 dark:text-zinc-500' => ! $day['is_current_month'] && ! $day['is_today'], + ])> + {{ $day['date']->format('j') }} +
-
- @foreach (array_slice($day['events'], 0, 3) as $event) -
($event['response_status'] ?? null) === 'DECLINED', - ]) - style="border-left: 3px {{ ($event['response_status'] ?? null) === 'NEEDS-ACTION' ? 'dashed' : 'solid' }} {{ $event['feed_color'] }}" - > - {{ $event['all_day'] ? __('All day') : $event['starts_at']->format('g:i A') }} - {{ $event['title'] }} -
- @endforeach +
+ @foreach (array_slice($day['events'], 0, 3) as $event) +
($event['response_status'] ?? null) === 'DECLINED', + ]) + style="border-left: 3px {{ ($event['response_status'] ?? null) === 'NEEDS-ACTION' ? 'dashed' : 'solid' }} {{ $event['feed_color'] }}" + > + {{ $event['all_day'] ? __('All day') : $event['starts_at']->format('g:i A') }} + {{ $event['title'] }} +
+ @endforeach - @if (count($day['events']) > 3) -
- {{ __('+:count more', ['count' => count($day['events']) - 3]) }} -
- @endif + @if (count($day['events']) > 3) +
+ {{ __('+:count more', ['count' => count($day['events']) - 3]) }} +
+ @endif +
-
- @endforeach + @endforeach +
diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" index 0574d9e..95d1ce9 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" @@ -31,18 +31,5 @@
- @switch($format) - @case('day') - - - @break - - @case('month') - - - @break - - @default - - @endswitch +
From ea4a948ad440a7da23a751abf57e1f89b305dccb Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 16:48:24 -0500 Subject: [PATCH 20/57] Add date and format to url, and adjust styles --- resources/views/components/kiosk/calendar/day.blade.php | 9 +++++---- .../views/pages/kiosk/\342\232\241calendar/calendar.php" | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/resources/views/components/kiosk/calendar/day.blade.php b/resources/views/components/kiosk/calendar/day.blade.php index ce237de..99491b5 100644 --- a/resources/views/components/kiosk/calendar/day.blade.php +++ b/resources/views/components/kiosk/calendar/day.blade.php @@ -1,5 +1,5 @@ -
-
+
+
-
+ +
diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" index 641a2ab..91677b8 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" @@ -7,12 +7,15 @@ use Illuminate\Support\Facades\Auth; use Livewire\Attributes\Computed; use Livewire\Attributes\Layout; +use Livewire\Attributes\Url; use Livewire\Component; new #[Layout('layouts::kiosk')] class extends Component { + #[Url] public string $focusedDate = ''; + #[Url] public string $format = 'week'; public array $selectedFeeds = []; From da91768d57fc28e1f3887a47b44f63363abc870d Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 16:48:39 -0500 Subject: [PATCH 21/57] Change day view to hours --- .../components/kiosk/calendar/day.blade.php | 60 +++++++++++++++++-- .../kiosk/\342\232\241calendar/calendar.php" | 38 +++++++++++- .../\342\232\241calendar/calendar.test.php" | 51 ++++++++++++++++ 3 files changed, 143 insertions(+), 6 deletions(-) diff --git a/resources/views/components/kiosk/calendar/day.blade.php b/resources/views/components/kiosk/calendar/day.blade.php index 99491b5..17a51b0 100644 --- a/resources/views/components/kiosk/calendar/day.blade.php +++ b/resources/views/components/kiosk/calendar/day.blade.php @@ -12,14 +12,64 @@
{{ $this->day['date']->format('F j') }}
-
- @if (count($this->day['events']) > 0) -
- @foreach ($this->day['events'] as $event) + @if (count($this->dayAllDayEvents) > 0) +
+
+ {{ __('All day') }} +
+ +
+ @foreach ($this->dayAllDayEvents as $event) @endforeach
- @endif +
+ @endif + +
+
+ @foreach (range(0, 23) as $hour) +
+ {{ Carbon\CarbonImmutable::createFromTime($hour)->format('g A') }} +
+ +
+ @endforeach + +
+ @foreach ($this->dayTimedEvents as $event) +
($event['response_status'] ?? null) === 'DECLINED', + ]) + style="top: {{ $event['timeline_top'] }}%; height: {{ $event['timeline_height'] }}%; left: 0; right: 0; min-height: 2.5rem; border-left: 4px {{ ($event['response_status'] ?? null) === 'NEEDS-ACTION' ? 'dashed' : 'solid' }} {{ $event['feed_color'] }}" + > +
+ {{ $event['starts_at']->format('g:i A') }} + + @if ($event['ends_at']) + {{ __('-') }} + {{ $event['ends_at']->format('g:i A') }} + @endif +
+ +
{{ $event['title'] }}
+ + @if ($event['location']) +
{{ $event['location'] }}
+ @endif +
+ @endforeach +
+
diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" index 91677b8..72555c8 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" @@ -56,6 +56,42 @@ public function day(): array ]; } + /** @return array> */ + #[Computed] + public function dayAllDayEvents(): array + { + return collect($this->dayEvents) + ->filter(fn (array $event): bool => $event['all_day']) + ->values() + ->all(); + } + + /** @return array> */ + #[Computed] + public function dayTimedEvents(): array + { + $dayStartsAt = $this->focusedDate()->startOfDay(); + $dayEndsAt = $this->focusedDate()->endOfDay(); + + return collect($this->dayEvents) + ->reject(fn (array $event): bool => $event['all_day']) + ->map(function (array $event) use ($dayEndsAt, $dayStartsAt): array { + $startsAt = $event['starts_at']->lessThan($dayStartsAt) ? $dayStartsAt : $event['starts_at']; + $endsAt = $event['ends_at'] instanceof CarbonImmutable ? $event['ends_at'] : $event['starts_at']->addMinutes(30); + $endsAt = $endsAt->greaterThan($dayEndsAt) ? $dayEndsAt : $endsAt; + $duration = max(15, (int) round($startsAt->diffInMinutes($endsAt))); + $startsAfterMidnight = (int) round($dayStartsAt->diffInMinutes($startsAt)); + + return [ + ...$event, + 'timeline_top' => ($startsAfterMidnight / 1440) * 100, + 'timeline_height' => ($duration / 1440) * 100, + ]; + }) + ->values() + ->all(); + } + /** @return array> */ #[Computed] public function weekEvents(): array @@ -188,7 +224,7 @@ private function eventsForRange(CarbonImmutable $startsAt, int $days): array private function clearCalendarState(): void { - unset($this->dayEvents, $this->day, $this->weekEvents, $this->weekDays, $this->monthEvents, $this->monthDays); + unset($this->dayEvents, $this->day, $this->dayAllDayEvents, $this->dayTimedEvents, $this->weekEvents, $this->weekDays, $this->monthEvents, $this->monthDays); } private function timezoneName(): string diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" index b5d7b23..eca241b 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" @@ -67,6 +67,57 @@ ->assertSee('May 9'); }); +test('day calendar shows hours and sizes timed events by duration', function () { + Http::fake([ + 'https://example.com/day-calendar.ics' => Http::response(<<<'ICS' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sunny//Tests//EN +BEGIN:VEVENT +UID:family-day +DTSTAMP:20260501T120000Z +DTSTART;VALUE=DATE:20260508 +DTEND;VALUE=DATE:20260509 +SUMMARY:Family Day +END:VEVENT +BEGIN:VEVENT +UID:morning-standup +DTSTAMP:20260501T120000Z +DTSTART;TZID=America/Chicago:20260508T090000 +DTEND;TZID=America/Chicago:20260508T103000 +SUMMARY:Morning Standup +LOCATION:Kitchen +END:VEVENT +END:VCALENDAR +ICS), + ]); + + $this->travelTo(Date::parse('2026-05-08 12:00:00', 'America/Chicago')); + + $team = Team::factory() + ->has(CalendarFeed::factory()->state([ + 'url' => 'https://example.com/day-calendar.ics', + 'name' => 'Family Calendar', + 'color' => CalendarColor::Blue, + ])) + ->create(); + $user = User::factory()->memberOf($team)->create(); + + Livewire::actingAs($user) + ->test('pages::kiosk.calendar') + ->set('format', 'day') + ->assertSee('12 AM') + ->assertSee('9 AM') + ->assertSee('All day') + ->assertSee('Family Day') + ->assertSee('Morning Standup') + ->assertSee('Kitchen') + ->assertSee('9:00 AM') + ->assertSee('10:30 AM') + ->assertSee('top: 37.5%', false) + ->assertSee('height: 6.25%', false); +}); + test('can go to the next and previous weeks', function () { $this->travelTo(Date::parse('2026-05-08')); From 3f295abe716b91be345bbf259abe11533374a971 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 16:52:21 -0500 Subject: [PATCH 22/57] Adjust styles --- resources/views/components/kiosk/calendar/day.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/components/kiosk/calendar/day.blade.php b/resources/views/components/kiosk/calendar/day.blade.php index 17a51b0..9a75d18 100644 --- a/resources/views/components/kiosk/calendar/day.blade.php +++ b/resources/views/components/kiosk/calendar/day.blade.php @@ -1,9 +1,9 @@ -
+
! $this->day['is_today'], 'bg-blue-100 dark:bg-blue-900' => $this->day['is_today'], ]) From 4c0b89f0fb21ea6e6ecf0aecfada245fa1c8a3d6 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 16:54:57 -0500 Subject: [PATCH 23/57] Change to button group --- .../pages/kiosk/\342\232\241calendar/calendar.blade.php" | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" index 95d1ce9..8ba66bc 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" @@ -25,9 +25,11 @@ - - {{ __('Today') }} - + + + {{ __('Today') }} + +
From a7b9e9d2aa52df3ca0c75a66d7f877ff5d156283 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 17:01:20 -0500 Subject: [PATCH 24/57] Adjust month view to scroll, and hide more than two events on month view --- .../components/kiosk/calendar/month.blade.php | 14 ++--- .../\342\232\241calendar/calendar.test.php" | 51 +++++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/resources/views/components/kiosk/calendar/month.blade.php b/resources/views/components/kiosk/calendar/month.blade.php index b0df4d6..47c53cb 100644 --- a/resources/views/components/kiosk/calendar/month.blade.php +++ b/resources/views/components/kiosk/calendar/month.blade.php @@ -1,9 +1,9 @@ -
-
+
+
{{ now()->format('F') }}
-
+
@foreach (range(0, 6) as $offset)
{{ $this->monthDays[$offset]['date']->format('D') }} @@ -11,7 +11,7 @@ @endforeach
-
+
@foreach ($this->monthDays as $day)
- @foreach (array_slice($day['events'], 0, 3) as $event) + @foreach (array_slice($day['events'], 0, 2) as $event)
@endforeach - @if (count($day['events']) > 3) + @if (count($day['events']) > 2)
- {{ __('+:count more', ['count' => count($day['events']) - 3]) }} + {{ __('+:count events', ['count' => count($day['events']) - 2]) }}
@endif
diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" index eca241b..29f74c9 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.test.php" @@ -169,6 +169,57 @@ ]); }); +test('month calendar shows two events before overflow count', function () { + Http::fake([ + 'https://example.com/month-calendar.ics' => Http::response(<<<'ICS' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sunny//Tests//EN +BEGIN:VEVENT +UID:first-event +DTSTAMP:20260501T120000Z +DTSTART;TZID=America/Chicago:20260515T090000 +DTEND;TZID=America/Chicago:20260515T100000 +SUMMARY:First Event +END:VEVENT +BEGIN:VEVENT +UID:second-event +DTSTAMP:20260501T120000Z +DTSTART;TZID=America/Chicago:20260515T110000 +DTEND;TZID=America/Chicago:20260515T120000 +SUMMARY:Second Event +END:VEVENT +BEGIN:VEVENT +UID:third-event +DTSTAMP:20260501T120000Z +DTSTART;TZID=America/Chicago:20260515T130000 +DTEND;TZID=America/Chicago:20260515T140000 +SUMMARY:Third Event +END:VEVENT +END:VCALENDAR +ICS), + ]); + + $this->travelTo(Date::parse('2026-05-15 12:00:00', 'America/Chicago')); + + $team = Team::factory() + ->has(CalendarFeed::factory()->state([ + 'url' => 'https://example.com/month-calendar.ics', + 'name' => 'Family Calendar', + 'color' => CalendarColor::Blue, + ])) + ->create(); + $user = User::factory()->memberOf($team)->create(); + + Livewire::actingAs($user) + ->test('pages::kiosk.calendar') + ->set('format', 'month') + ->assertSee('First Event') + ->assertSee('Second Event') + ->assertSee('+1 events') + ->assertDontSee('Third Event'); +}); + test('can hide feed from calendar', function () { Http::allowStrayRequests([ 'https://calendar.google.com/calendar/ical/*', From 4a94c5ae8f5752e4ea7a04b9c23bb3c6d17fe10a Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Fri, 15 May 2026 17:22:25 -0500 Subject: [PATCH 25/57] =?UTF-8?q?Just=20Keep=20Swimming=20=F0=9F=90=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/views/layouts/app.blade.php | 4 ++++ .../pages/kiosk/\342\232\241calendar/calendar.blade.php" | 4 +--- .../views/pages/kiosk/\342\232\241calendar/calendar.php" | 6 ------ 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 05ce63a..111658d 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -21,6 +21,10 @@ {{ __('Recipes') }} + + {{ __('Kiosk') }} + + diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" index 8ba66bc..1209822 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.blade.php" @@ -1,8 +1,6 @@
-
- {{ $this->nowLabel }} -
+ {{ Carbon\CarbonImmutable::now($this->timezoneName())->format('D, M j g:i A') }}
diff --git "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" index 72555c8..3b44d8d 100644 --- "a/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" +++ "b/resources/views/pages/kiosk/\342\232\241calendar/calendar.php" @@ -152,12 +152,6 @@ public function monthDays(): array ->all(); } - #[Computed] - public function nowLabel(): string - { - return CarbonImmutable::now($this->timezoneName())->format('D, M j g:i A'); - } - public function previous(): void { $this->focusedDate = match ($this->format) { From fd74418a535ce8f2c4bcb9499a2e661c7b9a4f6f Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Sun, 24 May 2026 20:01:08 -0500 Subject: [PATCH 26/57] Add collapsible sidebar --- resources/views/layouts/app.blade.php | 39 +++++++++++++++------------ 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 111658d..e5ba331 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -4,33 +4,38 @@ @include('layouts.partials.head') - + + + + + + - - - {{ __('Dashboard') }} - - - - {{ __('Inventory') }} - + + {{ __('Dashboard') }} + - - {{ __('Recipes') }} - + + {{ __('Inventory') }} + - - {{ __('Kiosk') }} - + + {{ __('Recipes') }} + - + + {{ __('Kiosk') }} + - {{ __('Make a Suggestion') }} From e93966a768cbe41fe982b68d12e368fcda1a5e8c Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Sun, 24 May 2026 20:01:18 -0500 Subject: [PATCH 27/57] Add kiosk configure page --- .../kiosk/\342\232\241configure/configure.blade.php" | 8 ++++++++ .../pages/kiosk/\342\232\241configure/configure.php" | 8 ++++++++ .../pages/kiosk/\342\232\241configure/configure.test.php" | 8 ++++++++ routes/web.php | 2 ++ 4 files changed, 26 insertions(+) create mode 100644 "resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241configure/configure.php" create mode 100644 "resources/views/pages/kiosk/\342\232\241configure/configure.test.php" diff --git "a/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" "b/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" new file mode 100644 index 0000000..631a7e1 --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" @@ -0,0 +1,8 @@ + +
+
+ +
+ + +
diff --git "a/resources/views/pages/kiosk/\342\232\241configure/configure.php" "b/resources/views/pages/kiosk/\342\232\241configure/configure.php" new file mode 100644 index 0000000..008929e --- /dev/null +++ "b/resources/views/pages/kiosk/\342\232\241configure/configure.php" @@ -0,0 +1,8 @@ +assertStatus(200); +}); diff --git a/routes/web.php b/routes/web.php index 182863e..31f64a0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -24,6 +24,8 @@ ->middleware(['auth', 'verified', EnsureTeamMembership::class]) ->name('kiosk') ->group(function (): void { + Route::livewire('configure', 'pages::kiosk.configure')->name('.configure'); + Route::livewire('calendar', 'pages::kiosk.calendar')->name('.calendar'); Route::livewire('routines', 'pages::kiosk.routines')->name('.routines'); Route::livewire('chore-chart', 'pages::kiosk.chore-chart')->name('.chore-chart'); From fb7a5571001b26de0420f26605f3782994d3b239 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Sun, 24 May 2026 20:03:33 -0500 Subject: [PATCH 28/57] Add calendar feeds for Strawhat's holidays --- composer.lock | 2 +- database/seeders/StrawhatsSeeder.php | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index 2139677..2f19129 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "1bc20519780b28785b4ad3ae2a87155d", + "content-hash": "1e4cdaf58e2e0319de050fa8276e65a8", "packages": [ { "name": "aws/aws-crt-php", diff --git a/database/seeders/StrawhatsSeeder.php b/database/seeders/StrawhatsSeeder.php index c01bc44..766cc97 100644 --- a/database/seeders/StrawhatsSeeder.php +++ b/database/seeders/StrawhatsSeeder.php @@ -4,7 +4,9 @@ namespace Database\Seeders; +use App\Enums\CalendarColor; use App\Enums\ItemType; +use App\Models\CalendarFeed; use App\Models\Item; use App\Models\Recipe; use App\Models\Team; @@ -130,5 +132,22 @@ public function run(): void ['name' => 'Master Oda\'s Dinner Paparazzi! (Home)'], ) ->create(); + + CalendarFeed::factory() + ->for($strawhats) + ->count(10) + ->sequence( + ['name' => 'Brazilian Holidays (Luffy)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=BR&year=' . now('Y'), 'color' => CalendarColor::Green], + ['name' => 'Japanese Holidays (Zoro)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=JP&year=' . now('Y'), 'color' => CalendarColor::Red], + ['name' => 'Swedish Holidays (Nami)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=SE&year=' . now('Y'), 'color' => CalendarColor::Gold], + ['name' => 'South African Holidays (Usopp)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=ZA&year=' . now('Y'), 'color' => CalendarColor::Red], + ['name' => 'French Holidays (Sanji)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=FR&year=' . now('Y'), 'color' => CalendarColor::Blue], + ['name' => 'Canadian Holidays (Chopper)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=CA&year=' . now('Y'), 'color' => CalendarColor::Red], + ['name' => 'Russian Holidays (Robin)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=RU&year=' . now('Y'), 'color' => CalendarColor::Blue], + ['name' => 'American Holidays (Franky)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=US&year=' . now('Y'), 'color' => CalendarColor::Red], + ['name' => 'Austrian Holidays (Brook)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=AT&year=' . now('Y'), 'color' => CalendarColor::Red], + ['name' => 'Indian Holidays (Jinbe)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=IN&year=' . now('Y'), 'color' => CalendarColor::Orange], + ) + ->create(); } } From 3565b6df69d0e55f7b199d05f0f8295b5cdf36bc Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Sun, 24 May 2026 20:31:05 -0500 Subject: [PATCH 29/57] Fix year formatting --- database/seeders/StrawhatsSeeder.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/database/seeders/StrawhatsSeeder.php b/database/seeders/StrawhatsSeeder.php index 766cc97..e80c5ec 100644 --- a/database/seeders/StrawhatsSeeder.php +++ b/database/seeders/StrawhatsSeeder.php @@ -137,16 +137,16 @@ public function run(): void ->for($strawhats) ->count(10) ->sequence( - ['name' => 'Brazilian Holidays (Luffy)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=BR&year=' . now('Y'), 'color' => CalendarColor::Green], - ['name' => 'Japanese Holidays (Zoro)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=JP&year=' . now('Y'), 'color' => CalendarColor::Red], - ['name' => 'Swedish Holidays (Nami)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=SE&year=' . now('Y'), 'color' => CalendarColor::Gold], - ['name' => 'South African Holidays (Usopp)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=ZA&year=' . now('Y'), 'color' => CalendarColor::Red], - ['name' => 'French Holidays (Sanji)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=FR&year=' . now('Y'), 'color' => CalendarColor::Blue], - ['name' => 'Canadian Holidays (Chopper)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=CA&year=' . now('Y'), 'color' => CalendarColor::Red], - ['name' => 'Russian Holidays (Robin)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=RU&year=' . now('Y'), 'color' => CalendarColor::Blue], - ['name' => 'American Holidays (Franky)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=US&year=' . now('Y'), 'color' => CalendarColor::Red], - ['name' => 'Austrian Holidays (Brook)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=AT&year=' . now('Y'), 'color' => CalendarColor::Red], - ['name' => 'Indian Holidays (Jinbe)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=IN&year=' . now('Y'), 'color' => CalendarColor::Orange], + ['name' => 'Brazilian Holidays (Luffy)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=BR&year=' . now()->format('Y'), 'color' => CalendarColor::Green], + ['name' => 'Japanese Holidays (Zoro)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=JP&year=' . now()->format('Y'), 'color' => CalendarColor::Red], + ['name' => 'Swedish Holidays (Nami)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=SE&year=' . now()->format('Y'), 'color' => CalendarColor::Gold], + ['name' => 'South African Holidays (Usopp)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=ZA&year=' . now()->format('Y'), 'color' => CalendarColor::Red], + ['name' => 'French Holidays (Sanji)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=FR&year=' . now()->format('Y'), 'color' => CalendarColor::Blue], + ['name' => 'Canadian Holidays (Chopper)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=CA&year=' . now()->format('Y'), 'color' => CalendarColor::Red], + ['name' => 'Russian Holidays (Robin)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=RU&year=' . now()->format('Y'), 'color' => CalendarColor::Blue], + ['name' => 'American Holidays (Franky)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=US&year=' . now()->format('Y'), 'color' => CalendarColor::Red], + ['name' => 'Austrian Holidays (Brook)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=AT&year=' . now()->format('Y'), 'color' => CalendarColor::Red], + ['name' => 'Indian Holidays (Jinbe)', 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=IN&year=' . now()->format('Y'), 'color' => CalendarColor::Orange], ) ->create(); } From 784790e06c9d1c20cf4cc0900a671910a471460e Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Sun, 24 May 2026 20:31:32 -0500 Subject: [PATCH 30/57] Add local screensize widget --- .../views/components/kiosk/calendar/week.blade.php | 2 +- resources/views/components/screensize.blade.php | 10 ++++++++++ resources/views/layouts/app.blade.php | 2 ++ resources/views/layouts/auth.blade.php | 7 +++++++ resources/views/layouts/guest.blade.php | 2 ++ resources/views/layouts/kiosk.blade.php | 2 ++ 6 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 resources/views/components/screensize.blade.php diff --git a/resources/views/components/kiosk/calendar/week.blade.php b/resources/views/components/kiosk/calendar/week.blade.php index 6b300c0..115587c 100644 --- a/resources/views/components/kiosk/calendar/week.blade.php +++ b/resources/views/components/kiosk/calendar/week.blade.php @@ -1,4 +1,4 @@ -
+
@foreach ($this->weekDays as $day)
+ base + + + + + +
+@endenv diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index e5ba331..f15b584 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -112,6 +112,8 @@ class="w-full cursor-pointer" {{ $slot }} + + @persist('toast') diff --git a/resources/views/layouts/auth.blade.php b/resources/views/layouts/auth.blade.php index 394c5e6..13319cf 100644 --- a/resources/views/layouts/auth.blade.php +++ b/resources/views/layouts/auth.blade.php @@ -17,6 +17,13 @@
+ + + + @persist('toast') + + @endpersist + @fluxScripts diff --git a/resources/views/layouts/guest.blade.php b/resources/views/layouts/guest.blade.php index 90a91f3..cd487d9 100644 --- a/resources/views/layouts/guest.blade.php +++ b/resources/views/layouts/guest.blade.php @@ -15,6 +15,8 @@ {{ $slot }}
+ + @persist('toast') @endpersist diff --git a/resources/views/layouts/kiosk.blade.php b/resources/views/layouts/kiosk.blade.php index 114af06..8213247 100644 --- a/resources/views/layouts/kiosk.blade.php +++ b/resources/views/layouts/kiosk.blade.php @@ -18,6 +18,8 @@ {{ $slot }} + + @persist('toast') @endpersist From 802df37c2511ca1b6147f476cbf8cd6adf0dc4c4 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Sun, 24 May 2026 20:31:45 -0500 Subject: [PATCH 31/57] Start on configure page --- .../configure.blade.php" | 76 +++++++++++- .../\342\232\241configure/configure.php" | 115 +++++++++++++++++- .../\342\232\241configure/configure.test.php" | 110 ++++++++++++++++- 3 files changed, 293 insertions(+), 8 deletions(-) diff --git "a/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" "b/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" index 631a7e1..4615582 100644 --- "a/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" +++ "b/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" @@ -1,8 +1,76 @@ - -
-
- +
+
+
+
diff --git "a/resources/views/pages/kiosk/\342\232\241configure/configure.php" "b/resources/views/pages/kiosk/\342\232\241configure/configure.php" index 008929e..ad1cbb6 100644 --- "a/resources/views/pages/kiosk/\342\232\241configure/configure.php" +++ "b/resources/views/pages/kiosk/\342\232\241configure/configure.php" @@ -1,8 +1,119 @@ currentTeam + ->calendarFeeds() + ->orderBy('name') + ->get(); + } + + public function saveFeed(): void + { + $validated = $this->validate([ + 'feedName' => ['required', 'string', 'max:255'], + 'feedUrl' => ['required', 'url', 'max:2048'], + 'feedColor' => ['required', 'string', Rule::in($this->calendarColorValues())], + ]); + + $attributes = [ + 'name' => $validated['feedName'], + 'url' => $validated['feedUrl'], + 'color' => $validated['feedColor'], + ]; + + if ($this->editingFeedId) { + $this->feedQuery() + ->whereKey($this->editingFeedId) + ->firstOrFail() + ->update($attributes); + + Flux::toast(variant: 'success', text: __('Calendar feed updated.')); + } else { + $this->feedQuery()->create($attributes); + + Flux::toast(variant: 'success', text: __('Calendar feed added.')); + } + + unset($this->feeds); + + $this->resetFeedForm(); + } + + public function editFeed(int $feedId): void + { + $feed = $this->feedQuery() + ->whereKey($feedId) + ->firstOrFail(); + + $this->editingFeedId = $feed->id; + $this->feedName = $feed->name; + $this->feedUrl = $feed->url; + $this->feedColor = $feed->color; + + $this->resetValidation(); + } + + public function deleteFeed(int $feedId): void + { + $this->feedQuery() + ->whereKey($feedId) + ->firstOrFail() + ->delete(); + + if ($this->editingFeedId === $feedId) { + $this->resetFeedForm(); + } + + unset($this->feeds); + + Flux::toast(variant: 'success', text: __('Calendar feed removed.')); + } + + public function resetFeedForm(): void + { + $this->reset(['editingFeedId', 'feedName', 'feedUrl']); + $this->feedColor = CalendarColor::Blue->value; + $this->resetValidation(); + } + + /** @return array */ + public function calendarColors(): array + { + return CalendarColor::cases(); + } + + private function feedQuery() + { + return Auth::user()->currentTeam->calendarFeeds(); + } + + /** @return array */ + private function calendarColorValues(): array + { + return array_map( + fn (CalendarColor $color): string => $color->value, + CalendarColor::cases(), + ); + } +}; diff --git "a/resources/views/pages/kiosk/\342\232\241configure/configure.test.php" "b/resources/views/pages/kiosk/\342\232\241configure/configure.test.php" index 6665156..eaa3acb 100644 --- "a/resources/views/pages/kiosk/\342\232\241configure/configure.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241configure/configure.test.php" @@ -1,8 +1,114 @@ assertStatus(200); + $team = Team::factory() + ->has(CalendarFeed::factory()->state([ + 'name' => 'Family Calendar', + 'url' => 'https://example.com/family.ics', + 'color' => CalendarColor::Green, + ])) + ->create(); + $user = User::factory()->memberOf($team)->create(); + + actingAs($user) + ->get(route('kiosk.configure')) + ->assertOk() + ->assertSee('Family Calendar'); + + Livewire::actingAs($user) + ->test('pages::kiosk.configure') + ->assertStatus(200) + ->assertSee('Family Calendar'); +}); + +it('can add a calendar feed', function () { + $team = Team::factory()->create(); + $user = User::factory()->memberOf($team)->create(); + + Livewire::actingAs($user) + ->test('pages::kiosk.configure') + ->set('feedName', 'Brazilian Holidays') + ->set('feedUrl', 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=BR&year=2026') + ->set('feedColor', CalendarColor::Green->value) + ->call('saveFeed') + ->assertHasNoErrors() + ->assertSet('feedName', '') + ->assertSet('feedUrl', '') + ->assertSet('feedColor', CalendarColor::Blue->value); + + $this->assertDatabaseHas('calendar_feeds', [ + 'team_id' => $team->id, + 'name' => 'Brazilian Holidays', + 'url' => 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=BR&year=2026', + 'color' => CalendarColor::Green->value, + ]); +}); + +it('can edit a team calendar feed', function () { + $team = Team::factory() + ->has(CalendarFeed::factory()->state([ + 'name' => 'US Holidays', + 'url' => 'https://example.com/us.ics', + 'color' => CalendarColor::Red, + ])) + ->create(); + $user = User::factory()->memberOf($team)->create(); + $feed = $team->calendarFeeds()->firstOrFail(); + + Livewire::actingAs($user) + ->test('pages::kiosk.configure') + ->call('editFeed', $feed->id) + ->assertSet('editingFeedId', $feed->id) + ->assertSet('feedName', 'US Holidays') + ->set('feedName', 'American Holidays') + ->set('feedUrl', 'https://example.com/american.ics') + ->set('feedColor', CalendarColor::Indigo->value) + ->call('saveFeed') + ->assertHasNoErrors() + ->assertSet('editingFeedId', null); + + $this->assertDatabaseHas('calendar_feeds', [ + 'id' => $feed->id, + 'team_id' => $team->id, + 'name' => 'American Holidays', + 'url' => 'https://example.com/american.ics', + 'color' => CalendarColor::Indigo->value, + ]); +}); + +it('can remove a team calendar feed', function () { + $team = Team::factory() + ->has(CalendarFeed::factory()) + ->create(); + $user = User::factory()->memberOf($team)->create(); + $feed = $team->calendarFeeds()->firstOrFail(); + + Livewire::actingAs($user) + ->test('pages::kiosk.configure') + ->call('deleteFeed', $feed->id) + ->assertHasNoErrors(); + + $this->assertDatabaseMissing('calendar_feeds', [ + 'id' => $feed->id, + ]); +}); + +it('validates calendar feed input', function () { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test('pages::kiosk.configure') + ->set('feedName', '') + ->set('feedUrl', 'not-a-url') + ->set('feedColor', '#ffffff') + ->call('saveFeed') + ->assertHasErrors(['feedName', 'feedUrl', 'feedColor']); }); From d048fb00e0ab4643da2fd6f3671ed3b0e7d6f41a Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Sun, 24 May 2026 20:39:31 -0500 Subject: [PATCH 32/57] Add actions and form --- app/Actions/Kiosk/CreateFeed.php | 17 ++++++ app/Actions/Kiosk/UpdateFeed.php | 18 ++++++ app/Livewire/Forms/Kiosk/CalendarFeedForm.php | 59 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 app/Actions/Kiosk/CreateFeed.php create mode 100644 app/Actions/Kiosk/UpdateFeed.php create mode 100644 app/Livewire/Forms/Kiosk/CalendarFeedForm.php diff --git a/app/Actions/Kiosk/CreateFeed.php b/app/Actions/Kiosk/CreateFeed.php new file mode 100644 index 0000000..58cc9c5 --- /dev/null +++ b/app/Actions/Kiosk/CreateFeed.php @@ -0,0 +1,17 @@ + $data */ + public function handle(Team $team, array $data): CalendarFeed + { + return $team->calendarFeeds()->create($data); + } +} diff --git a/app/Actions/Kiosk/UpdateFeed.php b/app/Actions/Kiosk/UpdateFeed.php new file mode 100644 index 0000000..4e201f2 --- /dev/null +++ b/app/Actions/Kiosk/UpdateFeed.php @@ -0,0 +1,18 @@ + $data */ + public function handle(CalendarFeed $feed, array $data): CalendarFeed + { + $feed->update($data); + + return $feed; + } +} diff --git a/app/Livewire/Forms/Kiosk/CalendarFeedForm.php b/app/Livewire/Forms/Kiosk/CalendarFeedForm.php new file mode 100644 index 0000000..c78576c --- /dev/null +++ b/app/Livewire/Forms/Kiosk/CalendarFeedForm.php @@ -0,0 +1,59 @@ +value; + + public function load(CalendarFeed $feed): void + { + $this->fill([ + 'editingFeed' => $feed, + 'name' => $feed->name, + 'url' => $feed->url, + 'color' => $feed->color->value, + ]); + } + + public function save(): void + { + $data = $this->validate(); + + if ($this->editingItem) { + (new UpdateFeed)->handle($this->editingFeed, $data); + } else { + (new CreateFeed)->handle(Auth::user()->currentTeam, $data); + } + + $this->reset(); + } + + /** @return array */ + protected function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'url' => ['required', 'string', 'url'], + 'color' => ['required', Rule::enum(CalendarColor::class)], + ]; + } +} From 4ed6ca9afce632a00c4e12f19815f791dc9bbfea Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Sun, 24 May 2026 21:24:16 -0500 Subject: [PATCH 33/57] Clean up configure form --- app/Livewire/Forms/Kiosk/CalendarFeedForm.php | 6 +- app/Models/CalendarFeed.php | 9 ++ app/Policies/CalendarFeedPolicy.php | 36 ++++++ .../pages/kiosk/modals/feed-form.blade.php | 29 +++++ .../configure.blade.php" | 58 ++++------ .../\342\232\241configure/configure.php" | 105 ++++-------------- .../\342\232\241configure/configure.test.php" | 60 +++++----- 7 files changed, 147 insertions(+), 156 deletions(-) create mode 100644 app/Policies/CalendarFeedPolicy.php create mode 100644 resources/views/pages/kiosk/modals/feed-form.blade.php diff --git a/app/Livewire/Forms/Kiosk/CalendarFeedForm.php b/app/Livewire/Forms/Kiosk/CalendarFeedForm.php index c78576c..9d0104a 100644 --- a/app/Livewire/Forms/Kiosk/CalendarFeedForm.php +++ b/app/Livewire/Forms/Kiosk/CalendarFeedForm.php @@ -8,15 +8,13 @@ use App\Actions\Kiosk\UpdateFeed; use App\Enums\CalendarColor; use App\Models\CalendarFeed; -use App\Models\Item; use Illuminate\Support\Facades\Auth; use Illuminate\Validation\Rule; use Livewire\Form; -/** @package App\Livewire\Forms\Kiosk */ class CalendarFeedForm extends Form { - public ?Item $editingFeed = null; + public ?CalendarFeed $editingFeed = null; public string $name = ''; @@ -38,7 +36,7 @@ public function save(): void { $data = $this->validate(); - if ($this->editingItem) { + if ($this->editingFeed) { (new UpdateFeed)->handle($this->editingFeed, $data); } else { (new CreateFeed)->handle(Auth::user()->currentTeam, $data); diff --git a/app/Models/CalendarFeed.php b/app/Models/CalendarFeed.php index a1100ae..0535bd0 100644 --- a/app/Models/CalendarFeed.php +++ b/app/Models/CalendarFeed.php @@ -4,6 +4,7 @@ namespace App\Models; +use App\Enums\CalendarColor; use Database\Factories\CalendarFeedFactory; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -21,4 +22,12 @@ public function team(): BelongsTo { return $this->belongsTo(Team::class); } + + /** @return array */ + protected function casts(): array + { + return [ + 'color' => CalendarColor::class, + ]; + } } diff --git a/app/Policies/CalendarFeedPolicy.php b/app/Policies/CalendarFeedPolicy.php new file mode 100644 index 0000000..a4e6391 --- /dev/null +++ b/app/Policies/CalendarFeedPolicy.php @@ -0,0 +1,36 @@ +team_id === $user->current_team_id; + } + + public function create(User $user): bool + { + return true; + } + + public function update(User $user, CalendarFeed $feed): bool + { + return $feed->team_id === $user->current_team_id; + } + + public function delete(User $user, CalendarFeed $feed): bool + { + return $feed->team_id === $user->current_team_id; + } +} diff --git a/resources/views/pages/kiosk/modals/feed-form.blade.php b/resources/views/pages/kiosk/modals/feed-form.blade.php new file mode 100644 index 0000000..cfea931 --- /dev/null +++ b/resources/views/pages/kiosk/modals/feed-form.blade.php @@ -0,0 +1,29 @@ +@teleport('body') + +
+ {{ $form->editingFeed ? __('Edit Feed') : __('Add Feed') }} + + + + + + + @foreach (\App\Enums\CalendarColor::cases() as $color) + +
+
{{ ucfirst($color->name) }} +
+
+ @endforeach +
+ +
+ + + {{ __('Cancel') }} + + {{ $form->editingFeed ? __('Update') : __('Create') }} +
+ +
+@endteleport diff --git "a/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" "b/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" index 4615582..eb13c60 100644 --- "a/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" +++ "b/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" @@ -1,43 +1,21 @@ -
-
- +
+
+ {{ __('Kiosk Configuration') }} + {{ __('Preview your kiosk and manage its data sources.') }}
- + + @include('pages.kiosk.modals.feed-form') +
diff --git "a/resources/views/pages/kiosk/\342\232\241configure/configure.php" "b/resources/views/pages/kiosk/\342\232\241configure/configure.php" index ad1cbb6..7c6f412 100644 --- "a/resources/views/pages/kiosk/\342\232\241configure/configure.php" +++ "b/resources/views/pages/kiosk/\342\232\241configure/configure.php" @@ -1,119 +1,54 @@ currentTeam - ->calendarFeeds() - ->orderBy('name') - ->get(); + return Auth::user()->currentTeam->calendarFeeds()->orderBy('name')->get(); } - public function saveFeed(): void + public function delete(int $id): void { - $validated = $this->validate([ - 'feedName' => ['required', 'string', 'max:255'], - 'feedUrl' => ['required', 'url', 'max:2048'], - 'feedColor' => ['required', 'string', Rule::in($this->calendarColorValues())], - ]); - - $attributes = [ - 'name' => $validated['feedName'], - 'url' => $validated['feedUrl'], - 'color' => $validated['feedColor'], - ]; + $feed = $this->feeds->firstWhere('id', $id); - if ($this->editingFeedId) { - $this->feedQuery() - ->whereKey($this->editingFeedId) - ->firstOrFail() - ->update($attributes); + $this->authorize('delete', $feed); - Flux::toast(variant: 'success', text: __('Calendar feed updated.')); - } else { - $this->feedQuery()->create($attributes); - - Flux::toast(variant: 'success', text: __('Calendar feed added.')); - } + $feed->delete(); unset($this->feeds); - - $this->resetFeedForm(); + Flux::toast(variant: 'success', text: __('Calendar feed removed.')); } - public function editFeed(int $feedId): void + public function edit(int $id): void { - $feed = $this->feedQuery() - ->whereKey($feedId) - ->firstOrFail(); + $feed = $this->feeds->firstWhere('id', $id); - $this->editingFeedId = $feed->id; - $this->feedName = $feed->name; - $this->feedUrl = $feed->url; - $this->feedColor = $feed->color; + $this->authorize('update', $feed); - $this->resetValidation(); + $this->form->load($feed); + $this->modal('feed-form')->show(); } - public function deleteFeed(int $feedId): void + public function save(): void { - $this->feedQuery() - ->whereKey($feedId) - ->firstOrFail() - ->delete(); - - if ($this->editingFeedId === $feedId) { - $this->resetFeedForm(); + if ($this->form->editingFeed) { + $this->authorize('update', $this->form->editingFeed); + } else { + $this->authorize('create', CalendarFeed::class); } + $this->form->save(); + $this->modal('feed-form')->close(); unset($this->feeds); - - Flux::toast(variant: 'success', text: __('Calendar feed removed.')); - } - - public function resetFeedForm(): void - { - $this->reset(['editingFeedId', 'feedName', 'feedUrl']); - $this->feedColor = CalendarColor::Blue->value; - $this->resetValidation(); - } - - /** @return array */ - public function calendarColors(): array - { - return CalendarColor::cases(); - } - - private function feedQuery() - { - return Auth::user()->currentTeam->calendarFeeds(); - } - - /** @return array */ - private function calendarColorValues(): array - { - return array_map( - fn (CalendarColor $color): string => $color->value, - CalendarColor::cases(), - ); } }; diff --git "a/resources/views/pages/kiosk/\342\232\241configure/configure.test.php" "b/resources/views/pages/kiosk/\342\232\241configure/configure.test.php" index eaa3acb..b0d995c 100644 --- "a/resources/views/pages/kiosk/\342\232\241configure/configure.test.php" +++ "b/resources/views/pages/kiosk/\342\232\241configure/configure.test.php" @@ -8,7 +8,7 @@ use function Pest\Laravel\actingAs; -it('renders successfully', function () { +test('renders successfully', function () { $team = Team::factory() ->has(CalendarFeed::factory()->state([ 'name' => 'Family Calendar', @@ -29,20 +29,20 @@ ->assertSee('Family Calendar'); }); -it('can add a calendar feed', function () { +test('can add a calendar feed', function () { $team = Team::factory()->create(); $user = User::factory()->memberOf($team)->create(); Livewire::actingAs($user) ->test('pages::kiosk.configure') - ->set('feedName', 'Brazilian Holidays') - ->set('feedUrl', 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=BR&year=2026') - ->set('feedColor', CalendarColor::Green->value) - ->call('saveFeed') + ->set('form.name', 'Brazilian Holidays') + ->set('form.url', 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=BR&year=2026') + ->set('form.color', CalendarColor::Green->value) + ->call('save') ->assertHasNoErrors() - ->assertSet('feedName', '') - ->assertSet('feedUrl', '') - ->assertSet('feedColor', CalendarColor::Blue->value); + ->assertSet('form.name', '') + ->assertSet('form.url', '') + ->assertSet('form.color', CalendarColor::Blue->value); $this->assertDatabaseHas('calendar_feeds', [ 'team_id' => $team->id, @@ -52,7 +52,7 @@ ]); }); -it('can edit a team calendar feed', function () { +test('can edit a team calendar feed', function () { $team = Team::factory() ->has(CalendarFeed::factory()->state([ 'name' => 'US Holidays', @@ -65,15 +65,14 @@ Livewire::actingAs($user) ->test('pages::kiosk.configure') - ->call('editFeed', $feed->id) - ->assertSet('editingFeedId', $feed->id) - ->assertSet('feedName', 'US Holidays') - ->set('feedName', 'American Holidays') - ->set('feedUrl', 'https://example.com/american.ics') - ->set('feedColor', CalendarColor::Indigo->value) - ->call('saveFeed') + ->call('edit', $feed->id) + ->assertSet('form.name', 'US Holidays') + ->set('form.name', 'American Holidays') + ->set('form.url', 'https://example.com/american.ics') + ->set('form.color', CalendarColor::Indigo->value) + ->call('save') ->assertHasNoErrors() - ->assertSet('editingFeedId', null); + ->assertSet('editingFeed', null); $this->assertDatabaseHas('calendar_feeds', [ 'id' => $feed->id, @@ -84,31 +83,36 @@ ]); }); -it('can remove a team calendar feed', function () { +test('can remove a team calendar feed', function () { $team = Team::factory() - ->has(CalendarFeed::factory()) + ->has(CalendarFeed::factory()->state([ + 'name' => 'US Holidays', + 'url' => 'https://example.com/us.ics', + 'color' => CalendarColor::Red, + ])) ->create(); $user = User::factory()->memberOf($team)->create(); $feed = $team->calendarFeeds()->firstOrFail(); Livewire::actingAs($user) ->test('pages::kiosk.configure') - ->call('deleteFeed', $feed->id) - ->assertHasNoErrors(); + ->call('delete', $feed->id) + ->assertHasNoErrors() + ->assertDontSee('US Holidays'); $this->assertDatabaseMissing('calendar_feeds', [ 'id' => $feed->id, ]); }); -it('validates calendar feed input', function () { +test('validates calendar feed input', function () { $user = User::factory()->create(); Livewire::actingAs($user) ->test('pages::kiosk.configure') - ->set('feedName', '') - ->set('feedUrl', 'not-a-url') - ->set('feedColor', '#ffffff') - ->call('saveFeed') - ->assertHasErrors(['feedName', 'feedUrl', 'feedColor']); + ->set('form.name', '') + ->set('form.url', 'not-a-url') + ->set('form.color', '#ffffff') + ->call('save') + ->assertHasErrors(['form.name', 'form.url', 'form.color']); }); From d210de38ba4f2a81526aa53f9584ed9d9951f75d Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Sun, 24 May 2026 21:30:59 -0500 Subject: [PATCH 34/57] Add tabs --- .../configure.blade.php" | 101 ++++++++++-------- 1 file changed, 59 insertions(+), 42 deletions(-) diff --git "a/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" "b/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" index eb13c60..fc191a3 100644 --- "a/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" +++ "b/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" @@ -1,56 +1,73 @@ -
+
{{ __('Kiosk Configuration') }} {{ __('Preview your kiosk and manage its data sources.') }}
-
-
- -
- -
-
- {{ __('Calendar feeds') }} - - {{ __('Add Calendar Feed') }} - -
+
+ +
-
- @forelse ($this->feeds as $feed) -
-
-
-
- - {{ $feed->name }} -
+ + + Calendar + Routines + Chores + Lists + Meals + Settings + - - {{ $feed->url }} - -
+ +
+
+ {{ __('Calendar feeds') }} + + {{ __('Add Calendar Feed') }} + +
+ +
+ @forelse ($this->feeds as $feed) +
+
+
+
+ + {{ $feed->name }} +
-
- - - + + {{ $feed->url }} + +
- - - +
+ + + + + + + +
-
- @empty -
- {{ __('No calendar feeds yet.') }} -
- @endforelse + @empty +
+ {{ __('No calendar feeds yet.') }} +
+ @endforelse +
-
+ + ... + ... + ... + ... + ... + - @include('pages.kiosk.modals.feed-form') -
+ + @include('pages.kiosk.modals.feed-form')
From 00bbb13301364502f54678a35a5c8f85865a0e58 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Sun, 24 May 2026 21:35:39 -0500 Subject: [PATCH 35/57] Add flux components --- .../\342\232\241configure/configure.blade.php" | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git "a/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" "b/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" index fc191a3..08f4495 100644 --- "a/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" +++ "b/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" @@ -19,7 +19,7 @@ -
+
{{ __('Calendar feeds') }} @@ -29,17 +29,15 @@
@forelse ($this->feeds as $feed) -
+
-
+ {{ $feed->name }} -
+ - - {{ $feed->url }} - + {{ $feed->url }}
@@ -52,14 +50,14 @@
-
+ @empty
{{ __('No calendar feeds yet.') }}
@endforelse
-
+
... ... From fbc75de9d95c5169a9ec075376e69b1452108121 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Tue, 26 May 2026 09:28:12 -0500 Subject: [PATCH 36/57] =?UTF-8?q?Just=20Keep=20Swimming=20=F0=9F=90=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Livewire/Forms/Kiosk/SettingsForm.php | 16 ++++- app/Models/Team.php | 3 +- config/services.php | 4 ++ ..._14_211119_add_timezone_to_teams_table.php | 1 + resources/views/layouts/app.blade.php | 2 +- .../views/layouts/kiosk-configure.blade.php | 25 +++++++ resources/views/layouts/kiosk.blade.php | 1 - .../views/layouts/partials/head.blade.php | 1 + .../\342\232\241calendar/calendar.blade.php" | 41 +++++++++++ .../\342\232\241calendar/calendar.php" | 3 +- .../\342\232\241calendar/calendar.test.php" | 12 ++-- .../configure/\342\232\241preview.blade.php" | 43 +++++++++++ .../\342\232\241settings/settings.blade.php" | 48 +++++++++++++ .../\342\232\241settings/settings.php" | 2 +- .../\342\232\241settings/settings.test.php" | 0 .../configure.blade.php" | 71 ------------------- .../\342\232\241settings/settings.blade.php" | 19 ----- routes/web.php | 5 +- 18 files changed, 193 insertions(+), 104 deletions(-) create mode 100644 resources/views/layouts/kiosk-configure.blade.php create mode 100644 "resources/views/pages/kiosk/configure/\342\232\241calendar/calendar.blade.php" rename "resources/views/pages/kiosk/\342\232\241configure/configure.php" => "resources/views/pages/kiosk/configure/\342\232\241calendar/calendar.php" (92%) rename "resources/views/pages/kiosk/\342\232\241configure/configure.test.php" => "resources/views/pages/kiosk/configure/\342\232\241calendar/calendar.test.php" (92%) create mode 100644 "resources/views/pages/kiosk/configure/\342\232\241preview.blade.php" create mode 100644 "resources/views/pages/kiosk/configure/\342\232\241settings/settings.blade.php" rename "resources/views/pages/kiosk/\342\232\241settings/settings.php" => "resources/views/pages/kiosk/configure/\342\232\241settings/settings.php" (83%) rename "resources/views/pages/kiosk/\342\232\241settings/settings.test.php" => "resources/views/pages/kiosk/configure/\342\232\241settings/settings.test.php" (100%) delete mode 100644 "resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" delete mode 100644 "resources/views/pages/kiosk/\342\232\241settings/settings.blade.php" diff --git a/app/Livewire/Forms/Kiosk/SettingsForm.php b/app/Livewire/Forms/Kiosk/SettingsForm.php index d1aa026..db38ea3 100644 --- a/app/Livewire/Forms/Kiosk/SettingsForm.php +++ b/app/Livewire/Forms/Kiosk/SettingsForm.php @@ -19,17 +19,31 @@ class SettingsForm extends Form #[Validate('required|int|between:0,6')] public int $week_start = Carbon::SUNDAY; + #[Validate([ + 'address' => 'required|array', + 'address.*' => 'required', + ])] + public array $address = [ + 'address' => '', + 'city' => '', + 'state' => '', + 'zip' => '', + 'lat' => '', + 'long' => '', + ]; + public function load(Team $team) { $this->editingTeam = $team; $this->timezone = $team->timezone; $this->week_start = $team->week_start; + $this->address = $team->address; } public function save() { $data = $this->validate(); - $this->editingTeam->query()->update($data); + $this->editingTeam->update($data); } } diff --git a/app/Models/Team.php b/app/Models/Team.php index 57194e2..fd12c28 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Eloquent\SoftDeletes; -#[Fillable(['name', 'slug', 'is_personal'])] +#[Fillable(['name', 'slug', 'is_personal', 'address'])] class Team extends Model { /** @use HasFactory */ @@ -106,6 +106,7 @@ protected function casts(): array { return [ 'is_personal' => 'boolean', + 'address' => 'array', ]; } } diff --git a/config/services.php b/config/services.php index 6a90eb8..c30a252 100644 --- a/config/services.php +++ b/config/services.php @@ -14,6 +14,10 @@ | */ + 'mapbox' => [ + 'key' => env('MAPBOX_API_KEY'), + ], + 'postmark' => [ 'key' => env('POSTMARK_API_KEY'), ], diff --git a/database/migrations/2026_05_14_211119_add_timezone_to_teams_table.php b/database/migrations/2026_05_14_211119_add_timezone_to_teams_table.php index ad39cf5..9290e3a 100644 --- a/database/migrations/2026_05_14_211119_add_timezone_to_teams_table.php +++ b/database/migrations/2026_05_14_211119_add_timezone_to_teams_table.php @@ -12,6 +12,7 @@ public function up(): void Schema::table('teams', function (Blueprint $table) { $table->string('timezone')->default('America/Chicago')->after('is_personal'); $table->integer('week_start')->default(Carbon::SUNDAY)->after('timezone'); + $table->json('address')->nullable()->after('week_start'); }); } diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index f15b584..40e676c 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -29,7 +29,7 @@ {{ __('Recipes') }} - + {{ __('Kiosk') }} diff --git a/resources/views/layouts/kiosk-configure.blade.php b/resources/views/layouts/kiosk-configure.blade.php new file mode 100644 index 0000000..c960abc --- /dev/null +++ b/resources/views/layouts/kiosk-configure.blade.php @@ -0,0 +1,25 @@ + +
+ {{ __('Kiosk Configuration') }} + {{ __('Preview your kiosk and manage its data sources') }} + +
+ +
+
+ + {{ __('Preview') }} + {{ __('Calendar') }} + {{ __('Settings') }} + +
+ + + +
+
+ {{ $slot }} +
+
+
+
diff --git a/resources/views/layouts/kiosk.blade.php b/resources/views/layouts/kiosk.blade.php index 8213247..c4ad432 100644 --- a/resources/views/layouts/kiosk.blade.php +++ b/resources/views/layouts/kiosk.blade.php @@ -11,7 +11,6 @@ {{ __('Chores') }} {{ __('Lists') }} {{ __('Meals') }} - {{ __('Settings') }} diff --git a/resources/views/layouts/partials/head.blade.php b/resources/views/layouts/partials/head.blade.php index 0665c3f..d6b2a30 100644 --- a/resources/views/layouts/partials/head.blade.php +++ b/resources/views/layouts/partials/head.blade.php @@ -28,3 +28,4 @@ @vite(['resources/css/app.css', 'resources/js/app.js']) @fluxAppearance +@stack('head') diff --git "a/resources/views/pages/kiosk/configure/\342\232\241calendar/calendar.blade.php" "b/resources/views/pages/kiosk/configure/\342\232\241calendar/calendar.blade.php" new file mode 100644 index 0000000..40975f3 --- /dev/null +++ "b/resources/views/pages/kiosk/configure/\342\232\241calendar/calendar.blade.php" @@ -0,0 +1,41 @@ +
+
+ {{ __('Calendar feeds') }} + + {{ __('Add Calendar Feed') }} + +
+ +
+ @forelse ($this->feeds as $feed) + +
+
+ + + {{ $feed->name }} + + + {{ $feed->url }} +
+ +
+ + + + + + + +
+
+
+ @empty +
+ {{ __('No calendar feeds yet.') }} +
+ @endforelse +
+ + @include('pages.kiosk.modals.feed-form') +
diff --git "a/resources/views/pages/kiosk/\342\232\241configure/configure.php" "b/resources/views/pages/kiosk/configure/\342\232\241calendar/calendar.php" similarity index 92% rename from "resources/views/pages/kiosk/\342\232\241configure/configure.php" rename to "resources/views/pages/kiosk/configure/\342\232\241calendar/calendar.php" index 7c6f412..2dc8453 100644 --- "a/resources/views/pages/kiosk/\342\232\241configure/configure.php" +++ "b/resources/views/pages/kiosk/configure/\342\232\241calendar/calendar.php" @@ -5,9 +5,10 @@ use Flux\Flux; use Illuminate\Support\Facades\Auth; use Livewire\Attributes\Computed; +use Livewire\Attributes\Layout; use Livewire\Component; -new class extends Component +new #[Layout('layouts::kiosk-configure')] class extends Component { public CalendarFeedForm $form; diff --git "a/resources/views/pages/kiosk/\342\232\241configure/configure.test.php" "b/resources/views/pages/kiosk/configure/\342\232\241calendar/calendar.test.php" similarity index 92% rename from "resources/views/pages/kiosk/\342\232\241configure/configure.test.php" rename to "resources/views/pages/kiosk/configure/\342\232\241calendar/calendar.test.php" index b0d995c..d67cba9 100644 --- "a/resources/views/pages/kiosk/\342\232\241configure/configure.test.php" +++ "b/resources/views/pages/kiosk/configure/\342\232\241calendar/calendar.test.php" @@ -19,12 +19,12 @@ $user = User::factory()->memberOf($team)->create(); actingAs($user) - ->get(route('kiosk.configure')) + ->get(route('kiosk.configure.calendar')) ->assertOk() ->assertSee('Family Calendar'); Livewire::actingAs($user) - ->test('pages::kiosk.configure') + ->test('pages::kiosk.configure.calendar') ->assertStatus(200) ->assertSee('Family Calendar'); }); @@ -34,7 +34,7 @@ $user = User::factory()->memberOf($team)->create(); Livewire::actingAs($user) - ->test('pages::kiosk.configure') + ->test('pages::kiosk.configure.calendar') ->set('form.name', 'Brazilian Holidays') ->set('form.url', 'https://worldpublicholiday.com/calendar-feeds/feed.ics?country=BR&year=2026') ->set('form.color', CalendarColor::Green->value) @@ -64,7 +64,7 @@ $feed = $team->calendarFeeds()->firstOrFail(); Livewire::actingAs($user) - ->test('pages::kiosk.configure') + ->test('pages::kiosk.configure.calendar') ->call('edit', $feed->id) ->assertSet('form.name', 'US Holidays') ->set('form.name', 'American Holidays') @@ -95,7 +95,7 @@ $feed = $team->calendarFeeds()->firstOrFail(); Livewire::actingAs($user) - ->test('pages::kiosk.configure') + ->test('pages::kiosk.configure.calendar') ->call('delete', $feed->id) ->assertHasNoErrors() ->assertDontSee('US Holidays'); @@ -109,7 +109,7 @@ $user = User::factory()->create(); Livewire::actingAs($user) - ->test('pages::kiosk.configure') + ->test('pages::kiosk.configure.calendar') ->set('form.name', '') ->set('form.url', 'not-a-url') ->set('form.color', '#ffffff') diff --git "a/resources/views/pages/kiosk/configure/\342\232\241preview.blade.php" "b/resources/views/pages/kiosk/configure/\342\232\241preview.blade.php" new file mode 100644 index 0000000..0e6fc62 --- /dev/null +++ "b/resources/views/pages/kiosk/configure/\342\232\241preview.blade.php" @@ -0,0 +1,43 @@ +width; + $height = $this->height; + + $this->width = $height; + $this->height = $width; + } +}; +?> + +
+
+ + {{ __('Width') }} + + + px + + + + {{ __('Height') }} + + + px + + + +
+
+ +
+
diff --git "a/resources/views/pages/kiosk/configure/\342\232\241settings/settings.blade.php" "b/resources/views/pages/kiosk/configure/\342\232\241settings/settings.blade.php" new file mode 100644 index 0000000..0f3e869 --- /dev/null +++ "b/resources/views/pages/kiosk/configure/\342\232\241settings/settings.blade.php" @@ -0,0 +1,48 @@ +
+ + @foreach (DateTimeZone::listIdentifiers() as $timezoneOption) + {{ str_replace('_', ' ', $timezoneOption) }} + @endforeach + + + + {{ __('Sunday') }} + {{ __('Monday') }} + {{ __('Tuesday') }} + {{ __('Wednesday') }} + {{ __('Thursday') }} + {{ __('Friday') }} + {{ __('Saturday') }} + + + + Address for Weather + + + + + +
+ + + +
+
+ + Save +
+ +@assets + +@endassets + +@script + +@endscript diff --git "a/resources/views/pages/kiosk/\342\232\241settings/settings.php" "b/resources/views/pages/kiosk/configure/\342\232\241settings/settings.php" similarity index 83% rename from "resources/views/pages/kiosk/\342\232\241settings/settings.php" rename to "resources/views/pages/kiosk/configure/\342\232\241settings/settings.php" index 48e79e2..2cd6cbf 100644 --- "a/resources/views/pages/kiosk/\342\232\241settings/settings.php" +++ "b/resources/views/pages/kiosk/configure/\342\232\241settings/settings.php" @@ -5,7 +5,7 @@ use Livewire\Attributes\Layout; use Livewire\Component; -new #[Layout('layouts::kiosk')] class extends Component +new #[Layout('layouts::kiosk-configure')] class extends Component { public SettingsForm $form; diff --git "a/resources/views/pages/kiosk/\342\232\241settings/settings.test.php" "b/resources/views/pages/kiosk/configure/\342\232\241settings/settings.test.php" similarity index 100% rename from "resources/views/pages/kiosk/\342\232\241settings/settings.test.php" rename to "resources/views/pages/kiosk/configure/\342\232\241settings/settings.test.php" diff --git "a/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" "b/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" deleted file mode 100644 index 08f4495..0000000 --- "a/resources/views/pages/kiosk/\342\232\241configure/configure.blade.php" +++ /dev/null @@ -1,71 +0,0 @@ -
-
- {{ __('Kiosk Configuration') }} - {{ __('Preview your kiosk and manage its data sources.') }} -
- -
- -
- - - - Calendar - Routines - Chores - Lists - Meals - Settings - - - - -
- {{ __('Calendar feeds') }} - - {{ __('Add Calendar Feed') }} - -
- -
- @forelse ($this->feeds as $feed) - -
-
- - - {{ $feed->name }} - - - {{ $feed->url }} -
- -
- - - - - - - -
-
-
- @empty -
- {{ __('No calendar feeds yet.') }} -
- @endforelse -
-
-
- ... - ... - ... - ... - ... -
- - - @include('pages.kiosk.modals.feed-form') -
diff --git "a/resources/views/pages/kiosk/\342\232\241settings/settings.blade.php" "b/resources/views/pages/kiosk/\342\232\241settings/settings.blade.php" deleted file mode 100644 index fff10e6..0000000 --- "a/resources/views/pages/kiosk/\342\232\241settings/settings.blade.php" +++ /dev/null @@ -1,19 +0,0 @@ -
- - @foreach (DateTimeZone::listIdentifiers() as $timezoneOption) - {{ str_replace('_', ' ', $timezoneOption) }} - @endforeach - - - - {{ __('Sunday') }} - {{ __('Monday') }} - {{ __('Tuesday') }} - {{ __('Wednesday') }} - {{ __('Thursday') }} - {{ __('Friday') }} - {{ __('Saturday') }} - - - Save -
diff --git a/routes/web.php b/routes/web.php index 31f64a0..a6c2748 100644 --- a/routes/web.php +++ b/routes/web.php @@ -24,12 +24,13 @@ ->middleware(['auth', 'verified', EnsureTeamMembership::class]) ->name('kiosk') ->group(function (): void { - Route::livewire('configure', 'pages::kiosk.configure')->name('.configure'); + Route::livewire('configure/preview', 'pages::kiosk.configure.preview')->name('.configure.preview'); + Route::livewire('configure/calendar', 'pages::kiosk.configure.calendar')->name('.configure.calendar'); + Route::livewire('configure/settings', 'pages::kiosk.configure.settings')->name('.configure.settings'); Route::livewire('calendar', 'pages::kiosk.calendar')->name('.calendar'); Route::livewire('routines', 'pages::kiosk.routines')->name('.routines'); Route::livewire('chore-chart', 'pages::kiosk.chore-chart')->name('.chore-chart'); Route::livewire('lists', 'pages::kiosk.lists')->name('.lists'); Route::livewire('meal-planning', 'pages::kiosk.meal-planning')->name('.meal-planning'); - Route::livewire('settings', 'pages::kiosk.settings')->name('.settings'); }); From 2ec6416f4bb4221614dcc931d25c85d03eda7551 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Tue, 26 May 2026 10:29:02 -0500 Subject: [PATCH 37/57] Install Saloon and start on OpenWeather --- .env.example | 4 ++ .../OpenWeather/OpenWeatherConnector.php | 23 ++++++ composer.json | 2 + composer.lock | 72 ++++++++++++++++++- config/services.php | 4 ++ 5 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 app/Http/Integrations/OpenWeather/OpenWeatherConnector.php diff --git a/.env.example b/.env.example index c0660ea..48b6515 100644 --- a/.env.example +++ b/.env.example @@ -63,3 +63,7 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" + +# Services +MAPBOX_API_KEY= +OPENWEATHER_API_KEY= diff --git a/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php b/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php new file mode 100644 index 0000000..6fa92d8 --- /dev/null +++ b/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php @@ -0,0 +1,23 @@ + config('services.openweather.key'), + ]; + } +} diff --git a/composer.json b/composer.json index fdd1cfb..7770227 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,8 @@ "livewire/livewire": "^4.1", "nunomaduro/essentials": "^1.2", "sabre/vobject": "^4.5", + "saloonphp/laravel-plugin": "^4.3", + "saloonphp/saloon": "^4.0", "spatie/simple-excel": "^3.9" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 2f19129..2455b85 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "1e4cdaf58e2e0319de050fa8276e65a8", + "content-hash": "8c12ca248c64e0a023e0a53357518065", "packages": [ { "name": "aws/aws-crt-php", @@ -5328,6 +5328,76 @@ }, "time": "2026-04-27T10:56:01+00:00" }, + { + "name": "saloonphp/laravel-plugin", + "version": "v4.3.0", + "source": { + "type": "git", + "url": "https://github.com/saloonphp/laravel-plugin.git", + "reference": "c9754084dabe1002000d01cc2192fd08cfa6079d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/saloonphp/laravel-plugin/zipball/c9754084dabe1002000d01cc2192fd08cfa6079d", + "reference": "c9754084dabe1002000d01cc2192fd08cfa6079d", + "shasum": "" + }, + "require": { + "illuminate/console": "^11.0 || ^12.39.0 || ^13.0", + "illuminate/support": "^11.0 || ^12.39.0 || ^13.0", + "php": "^8.2", + "saloonphp/saloon": "^4.0", + "symfony/finder": "^6.4 || ^7.0 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.48", + "laravel/pulse": "^1.4", + "laravel/telescope": "^5.16", + "orchestra/testbench": "^9.15 || ^10.7 || ^11.0", + "pestphp/pest": "^3.0|^4.0", + "phpstan/phpstan": "^1.10.57|^2.0.2" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Saloon": "Saloon\\Laravel\\Facades\\Saloon" + }, + "providers": [ + "Saloon\\Laravel\\SaloonServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Saloon\\Laravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sam Carrรฉ", + "email": "29132017+Sammyjo20@users.noreply.github.com", + "role": "Developer" + } + ], + "description": "The official Laravel plugin for Saloon", + "homepage": "https://github.com/saloonphp/laravel-plugin", + "keywords": [ + "api", + "api-integrations", + "saloon", + "saloonphp", + "sdk" + ], + "support": { + "source": "https://github.com/saloonphp/laravel-plugin/tree/v4.3.0" + }, + "time": "2026-04-23T23:22:12+00:00" + }, { "name": "saloonphp/saloon", "version": "v4.0.0", diff --git a/config/services.php b/config/services.php index c30a252..4f963f1 100644 --- a/config/services.php +++ b/config/services.php @@ -18,6 +18,10 @@ 'key' => env('MAPBOX_API_KEY'), ], + 'openweather' => [ + 'key' => env('OPENWEATHER_API_KEY'), + ], + 'postmark' => [ 'key' => env('POSTMARK_API_KEY'), ], From a7559eda916e008540abd2877beb6f0db7b12af0 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Tue, 26 May 2026 10:50:59 -0500 Subject: [PATCH 38/57] Start on OneCall --- .../OpenWeather/OpenWeatherConnector.php | 2 +- .../OpenWeather/Requests/OneCall.php | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 app/Http/Integrations/OpenWeather/Requests/OneCall.php diff --git a/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php b/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php index 6fa92d8..c73c54f 100644 --- a/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php +++ b/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php @@ -11,7 +11,7 @@ class OpenWeatherConnector extends Connector public function resolveBaseUrl(): string { - return 'https://api.openweathermap.org'; + return 'https://api.openweathermap.org/data/3.0/'; } protected function defaultQuery(): array diff --git a/app/Http/Integrations/OpenWeather/Requests/OneCall.php b/app/Http/Integrations/OpenWeather/Requests/OneCall.php new file mode 100644 index 0000000..9736a7e --- /dev/null +++ b/app/Http/Integrations/OpenWeather/Requests/OneCall.php @@ -0,0 +1,31 @@ + $this->lat, + 'lon' => $this->lon, + 'exclude' => $this->exclude, + ]); + } +} From 6592ad03344412f179468f59e94fce40ed095f41 Mon Sep 17 00:00:00 2001 From: techenby <6541180+techenby@users.noreply.github.com> Date: Tue, 26 May 2026 15:51:48 +0000 Subject: [PATCH 39/57] Rector fixes --- app/Http/Integrations/OpenWeather/OpenWeatherConnector.php | 2 ++ app/Http/Integrations/OpenWeather/Requests/OneCall.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php b/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php index c73c54f..226c4d4 100644 --- a/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php +++ b/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php @@ -1,5 +1,7 @@ Date: Tue, 26 May 2026 12:00:27 -0500 Subject: [PATCH 40/57] Start on weather tile --- .../OpenWeather/OpenWeatherConnector.php | 3 ++ .../weather-tile.blade.php" | 38 ++++++++++---- .../weather-tile.php" | 40 ++++++++++++++- .../weather-tile.test.php" | 50 +++++++++++++++++-- .../\342\232\241settings/settings.blade.php" | 4 +- 5 files changed, 119 insertions(+), 16 deletions(-) diff --git a/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php b/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php index 226c4d4..49bb347 100644 --- a/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php +++ b/app/Http/Integrations/OpenWeather/OpenWeatherConnector.php @@ -6,10 +6,12 @@ use Saloon\Http\Connector; use Saloon\Traits\Plugins\AcceptsJson; +use Saloon\Traits\Plugins\AlwaysThrowOnErrors; class OpenWeatherConnector extends Connector { use AcceptsJson; + use AlwaysThrowOnErrors; public function resolveBaseUrl(): string { @@ -20,6 +22,7 @@ protected function defaultQuery(): array { return [ 'appid' => config('services.openweather.key'), + 'units' => 'imperial', ]; } } diff --git "a/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.blade.php" "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.blade.php" index bcea664..06bd5d4 100644 --- "a/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.blade.php" +++ "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.blade.php" @@ -1,13 +1,31 @@
-
-
- Chicago - 63ยฐ + @if ($temp !== null) +
+
+ {{ $location }} + {{ $description }} +
+
+ {{ $temp }}ยฐ +
+ {{ $high }}ยฐ + {{ $low }}ยฐ +
+
-
- - 73ยฐ - 55ยฐ -
-
+ @else + +
+
+ + +
+
+ + + +
+
+
+ @endif
diff --git "a/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" index f1acb2f..fd66009 100644 --- "a/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" +++ "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" @@ -1,8 +1,46 @@ currentTeam; + + if (! ($team->address['lat'] ?? null) || ! ($team->address['long'] ?? null)) { + return; + } + + $weather = Cache::remember( + "weather:{$team->id}", + now()->addMinutes(30), + fn () => (new OpenWeatherConnector)->send( + new OneCall($team->address['lat'], $team->address['long'], 'minutely,hourly,alerts') + )->json(), + ); + + $this->location = $team->address['city'] ?? null; + $this->temp = round($weather['current']['temp']); + $this->high = round($weather['daily'][0]['temp']['max']); + $this->low = round($weather['daily'][0]['temp']['min']); + $this->description = $weather['current']['weather'][0]['description'] ?? null; + $this->icon = $weather['current']['weather'][0]['icon'] ?? null; + } }; diff --git "a/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.test.php" "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.test.php" index 5da4fad..6c84931 100644 --- "a/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.test.php" +++ "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.test.php" @@ -1,8 +1,52 @@ assertStatus(200); +test('renders weather from api', function () { + Saloon::fake([ + OneCall::class => MockResponse::make([ + 'current' => [ + 'temp' => 63.4, + 'weather' => [['description' => 'overcast clouds', 'icon' => '04d']], + ], + 'daily' => [ + ['temp' => ['max' => 73.2, 'min' => 55.1]], + ], + ]), + ]); + + $team = Team::factory()->create([ + 'address' => [ + 'address' => '123 Main St', + 'city' => 'Chicago', + 'state' => 'IL', + 'zip' => '60601', + 'lat' => '41.8781', + 'long' => '-87.6298', + ], + ]); + + $user = User::factory()->memberOf($team)->create(); + + Livewire::actingAs($user) + ->test('kiosk.weather-tile') + ->assertSee('Chicago') + ->assertSee('63ยฐ') + ->assertSee('73ยฐ') + ->assertSee('55ยฐ'); + + Saloon::assertSent(OneCall::class); +}); + +test('renders nothing without address coordinates', function () { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test('kiosk.weather-tile') + ->assertDontSee('ยฐ'); }); diff --git "a/resources/views/pages/kiosk/configure/\342\232\241settings/settings.blade.php" "b/resources/views/pages/kiosk/configure/\342\232\241settings/settings.blade.php" index 0f3e869..e627b83 100644 --- "a/resources/views/pages/kiosk/configure/\342\232\241settings/settings.blade.php" +++ "b/resources/views/pages/kiosk/configure/\342\232\241settings/settings.blade.php" @@ -41,8 +41,8 @@ const autofill = document.querySelector('mapbox-address-autofill'); autofill.addEventListener('retrieve', (event) => { - $wire.$set('form.address.lat', event.detail.features[0].geometry.coordinates[0]); - $wire.$set('form.address.long', event.detail.features[0].geometry.coordinates[1]); + $wire.$set('form.address.long', event.detail.features[0].geometry.coordinates[0]); + $wire.$set('form.address.lat', event.detail.features[0].geometry.coordinates[1]); }); @endscript From a84bc18417b6da57c7bd1c61298c82ebaffcf2c1 Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Tue, 26 May 2026 12:19:47 -0500 Subject: [PATCH 41/57] =?UTF-8?q?Just=20Keep=20Swimming=20=F0=9F=90=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../weather-tile.php" | 17 +++++++++-- .../weather-tile.test.php" | 29 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git "a/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" index fd66009..0af7546 100644 --- "a/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" +++ "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.php" @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Livewire\Component; +use Saloon\Exceptions\Request\RequestException; new class extends Component { @@ -31,11 +32,21 @@ public function mount(): void $weather = Cache::remember( "weather:{$team->id}", now()->addMinutes(30), - fn () => (new OpenWeatherConnector)->send( - new OneCall($team->address['lat'], $team->address['long'], 'minutely,hourly,alerts') - )->json(), + function () use ($team) { + try { + return (new OpenWeatherConnector)->send( + new OneCall($team->address['lat'], $team->address['long'], 'minutely,hourly,alerts') + )->json(); + } catch (RequestException) { + return null; + } + }, ); + if (! $weather) { + return; + } + $this->location = $team->address['city'] ?? null; $this->temp = round($weather['current']['temp']); $this->high = round($weather['daily'][0]['temp']['max']); diff --git "a/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.test.php" "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.test.php" index 6c84931..1cfc0d1 100644 --- "a/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.test.php" +++ "b/resources/views/components/kiosk/\342\232\241weather-tile/weather-tile.test.php" @@ -43,6 +43,35 @@ Saloon::assertSent(OneCall::class); }); +test('shows skeleton when api returns 429', function () { + Saloon::fake([ + OneCall::class => MockResponse::make( + ['cod' => 429, 'message' => 'Too Many Requests'], + 429, + ), + ]); + + $team = Team::factory()->create([ + 'address' => [ + 'address' => '123 Main St', + 'city' => 'Chicago', + 'state' => 'IL', + 'zip' => '60601', + 'lat' => '41.8781', + 'long' => '-87.6298', + ], + ]); + + $user = User::factory()->memberOf($team)->create(); + + Livewire::actingAs($user) + ->test('kiosk.weather-tile') + ->assertDontSee('ยฐ') + ->assertSee('shimmer'); + + Saloon::assertSent(OneCall::class); +}); + test('renders nothing without address coordinates', function () { $user = User::factory()->create(); From 84088402249a961f1d78c8a7a139165cc9f7e60b Mon Sep 17 00:00:00 2001 From: Andy Swick Date: Tue, 26 May 2026 12:31:03 -0500 Subject: [PATCH 42/57] Adjust css --- resources/views/components/kiosk/calendar/day.blade.php | 4 ++-- resources/views/components/kiosk/calendar/week.blade.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/views/components/kiosk/calendar/day.blade.php b/resources/views/components/kiosk/calendar/day.blade.php index 9a75d18..48d939c 100644 --- a/resources/views/components/kiosk/calendar/day.blade.php +++ b/resources/views/components/kiosk/calendar/day.blade.php @@ -1,4 +1,4 @@ -
+
-
+