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({
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('