diff --git a/resources/js/components/local-time.tsx b/resources/js/components/local-time.tsx index 3cece5c3a..16d907442 100644 --- a/resources/js/components/local-time.tsx +++ b/resources/js/components/local-time.tsx @@ -1,6 +1,6 @@ import { Clock } from 'lucide-react'; import { useSyncExternalStore } from 'react'; -import { formatUtcOffset, formatZonedTime } from '@/lib/utils'; +import { cn, formatUtcOffset, formatZonedTime } from '@/lib/utils'; const MINUTE = 60_000; @@ -21,7 +21,15 @@ function currentMinute(): number { * What time it is for someone who chose to share their timezone. It renders nothing on the server, where the * current time is not the reader's. */ -export function LocalTime({ timeZone }: { timeZone: string }) { +export function LocalTime({ + timeZone, + as: Element = 'li', + className, +}: { + timeZone: string; + as?: 'li' | 'p'; + className?: string; +}) { const minute = useSyncExternalStore(subscribe, currentMinute, () => null); if (minute === null) { @@ -31,7 +39,10 @@ export function LocalTime({ timeZone }: { timeZone: string }) { const now = new Date(minute); return ( -
  • + {' '} ({formatUtcOffset(now, timeZone)}) -
  • + ); } diff --git a/resources/js/components/user-preview.tsx b/resources/js/components/user-preview.tsx index c24f9da0b..07866a90b 100644 --- a/resources/js/components/user-preview.tsx +++ b/resources/js/components/user-preview.tsx @@ -17,6 +17,7 @@ import { LiveAvatarMarker, LiveStreamCard, } from '@/components/live-stream-card'; +import { LocalTime } from '@/components/local-time'; import { Button } from '@/components/ui/button'; import { UserAvatar } from '@/components/user-avatar'; import { UserSafetyMenu } from '@/components/user-safety-menu'; @@ -481,6 +482,14 @@ function UserPreviewContent({

    )} + {user.show_local_time && user.timezone && !data?.isUnavailable && ( + + )} +
    {data?.isUnavailable ? (

    diff --git a/tests/Feature/Controllers/UserPreviewControllerTest.php b/tests/Feature/Controllers/UserPreviewControllerTest.php index c77d6a70e..6b07715e1 100644 --- a/tests/Feature/Controllers/UserPreviewControllerTest.php +++ b/tests/Feature/Controllers/UserPreviewControllerTest.php @@ -172,3 +172,18 @@ ->assertJsonMissingPath('user.commits_count') ->assertJsonMissingPath('user.followers_count'); }); + +it('includes the shared local time in the user preview', function (): void { + User::factory()->create(['username' => 'octocat', 'timezone' => 'Europe/Berlin', 'show_local_time' => true]); + + $this->actingAs(User::factory()->create()) + ->getJson('/@octocat/preview') + ->assertOk() + ->assertJsonPath('user.timezone', 'Europe/Berlin') + ->assertJsonPath('user.show_local_time', true); + + $this->getJson('/@octocat/preview') + ->assertOk() + ->assertJsonPath('user.timezone', 'Europe/Berlin') + ->assertJsonPath('user.show_local_time', true); +}); diff --git a/tests/Unit/LocalTime.test.tsx b/tests/Unit/LocalTime.test.tsx new file mode 100644 index 000000000..fff15ec46 --- /dev/null +++ b/tests/Unit/LocalTime.test.tsx @@ -0,0 +1,37 @@ +/// +import assert from 'node:assert/strict'; +import * as React from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { afterEach, test, vi } from 'vite-plus/test'; + +vi.doMock('react', () => ({ + ...React, + useSyncExternalStore: (_subscribe: unknown, read: () => unknown) => read(), +})); + +const { LocalTime } = await import('../../resources/js/components/local-time'); + +afterEach(() => vi.useRealTimers()); + +test('shows the time in the given timezone with its offset', () => { + vi.useFakeTimers({ now: new Date('2026-07-01T10:06:30Z') }); + + const html = renderToStaticMarkup( + , + ); + const expected = new Date('2026-07-01T10:06:00Z').toLocaleTimeString( + undefined, + { hour: 'numeric', minute: '2-digit', timeZone: 'Europe/Berlin' }, + ); + + assert.ok(html.startsWith('

    (UTC +02:00)`)); +}); + +test('renders as a list item by default, for the profile details', () => { + const html = renderToStaticMarkup(); + + assert.ok(html.startsWith('