From 7f206bb803b03b2c06cfa552a43aa2594d06441b Mon Sep 17 00:00:00 2001 From: Dennis Petersmann Date: Fri, 23 Oct 2026 08:21:52 +0200 Subject: [PATCH] feat: show the local time in profile previews Profile previews now show the same local time line as the profile, under the bio, for people who chose to share it. LocalTime can render as a paragraph for this. --- resources/js/components/local-time.tsx | 19 ++++++++-- resources/js/components/user-preview.tsx | 9 +++++ .../Controllers/UserPreviewControllerTest.php | 15 ++++++++ tests/Unit/LocalTime.test.tsx | 37 +++++++++++++++++++ 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 tests/Unit/LocalTime.test.tsx 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('