diff --git a/app/Enums/EventTheme.php b/app/Enums/EventTheme.php index d851dd9f1f..c910abb344 100644 --- a/app/Enums/EventTheme.php +++ b/app/Enums/EventTheme.php @@ -7,6 +7,8 @@ use App\Dto\ReaderClock; use Carbon\CarbonInterface; use Illuminate\Http\Request; +use IntlCalendar; +use IntlGregorianCalendar; enum EventTheme: string { @@ -64,7 +66,7 @@ public static function current(?string $choice, CarbonInterface $date): ?self */ public static function choiceFrom(?string $cookie): string { - if ($cookie === self::DEFAULT || self::tryFrom((string) $cookie) instanceof self) { + if ($cookie !== null && ($cookie === self::DEFAULT || self::tryFrom($cookie) instanceof self)) { return $cookie; } @@ -88,6 +90,16 @@ public static function on(CarbonInterface $date): ?self private static function readerNow(Request $request): CarbonInterface { - return now(ReaderClock::of($request)->timeZone); + $now = now(); + $timeZone = ReaderClock::of($request)->timeZone; + + if ($timeZone === null) { + return $now; + } + + $calendar = new IntlGregorianCalendar($timeZone); + $calendar->setTime($now->getTimestampMs()); + + return $now->utcOffset(($calendar->get(IntlCalendar::FIELD_ZONE_OFFSET) + $calendar->get(IntlCalendar::FIELD_DST_OFFSET)) / 60000); } } diff --git a/composer.json b/composer.json index a456e10e1d..4b744f53c6 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,8 @@ "license": "MIT", "require": { "php": "^8.5.0", + "ext-intl": "*", + "lib-icu": ">=57.1", "firebase/php-jwt": "^7.2.1", "inertiajs/inertia-laravel": "^3.6.0", "intervention/image": "^4.4.1", diff --git a/composer.lock b/composer.lock index 24587fb3ca..e35b61f2e2 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": "ba88d229cb2087c4ed1e7cdcbaae1196", + "content-hash": "4b22ff4f9c97c3a0d36fe852ef4006f8", "packages": [ { "name": "aws/aws-crt-php", @@ -15894,7 +15894,9 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.5.0" + "php": "^8.5.0", + "ext-intl": "*", + "lib-icu": ">=57.1" }, "platform-dev": {}, "plugin-api-version": "2.9.0" diff --git a/tests/Feature/EventThemeTest.php b/tests/Feature/EventThemeTest.php index b21ca036ac..c999a1d840 100644 --- a/tests/Feature/EventThemeTest.php +++ b/tests/Feature/EventThemeTest.php @@ -36,6 +36,22 @@ ->assertSee('data-event-theme="halloween"', false); }); +it('uses browser timezone aliases for the theme and greeting year', function (string $date, string $timeZone, string $theme, int $year): void { + Date::setTestNow($date); + + $this->withUnencryptedCookie('time_zone', $timeZone) + ->get(route('home')) + ->assertOk() + ->assertInertia(fn ($page) => $page + ->where('eventTheme', $theme) + ->where('eventYear', $year)); +})->with([ + 'an eastern alias ahead of UTC' => ['2026-10-28 20:00:00 UTC', 'Asia/Calcutta', 'halloween', 2026], + 'a western alias during daylight saving time' => ['2026-10-29 04:30:00 UTC', 'US/Eastern', 'halloween', 2026], + 'a western alias after daylight saving time' => ['2026-11-02 00:30:00 UTC', 'US/Eastern', 'halloween', 2026], + 'a western alias still in the previous year' => ['2027-01-01 01:00:00 UTC', 'US/Eastern', 'newyear', 2027], +]); + it('shows the theme the reader picked, whatever the day', function (string $choice): void { Date::setTestNow('2026-06-15 12:00:00'); @@ -98,8 +114,8 @@ ->assertOk() ->assertInertia(fn ($page) => $page->where('eventYear', $year)); })->with([ - 'New Year\'s Eve' => ['2026-12-31 23:59:59', 2027], - 'New Year\'s Day' => ['2027-01-01 00:00:00', 2027], + "New Year's Eve" => ['2026-12-31 23:59:59', 2027], + "New Year's Day" => ['2027-01-01 00:00:00', 2027], 'the day after' => ['2027-01-02 12:00:00', 2027], ]); diff --git a/tests/Unit/Enums/EventThemeTest.php b/tests/Unit/Enums/EventThemeTest.php index 5658f0c421..954b1c1737 100644 --- a/tests/Unit/Enums/EventThemeTest.php +++ b/tests/Unit/Enums/EventThemeTest.php @@ -19,8 +19,8 @@ 'the day after Christmas' => ['2026-12-26 23:59:59', EventTheme::Christmas], 'between Christmas and New Year' => ['2026-12-29 23:59:59', null], 'two days before New Year' => ['2026-12-30 00:00:00', EventTheme::NewYear], - 'New Year\'s Eve' => ['2026-12-31 12:00:00', EventTheme::NewYear], - 'New Year\'s Day' => ['2027-01-01 12:00:00', EventTheme::NewYear], + "New Year's Eve" => ['2026-12-31 12:00:00', EventTheme::NewYear], + "New Year's Day" => ['2027-01-01 12:00:00', EventTheme::NewYear], 'the day after New Year' => ['2027-01-02 23:59:59', EventTheme::NewYear], 'after the New Year window' => ['2027-01-03 00:00:00', null], 'an ordinary day' => ['2026-06-15 12:00:00', null],