Skip to content

Commit 1c7cb9d

Browse files
authored
enhancement(ui): full preferences in profile view mode + date/time hover tooltips (#375)
User Profile: the read-only view now shows Date Format and Time Format (previously only visible while editing), so all preferences appear in both view and edit modes. Also pads the preferences card in edit mode (px-4) to match the read-only layout. DateFormatter: wrap the formatted value in a tooltip (on by default) that reveals the full date and time in the viewer's preferred format — handy when only the date or time is shown, or when the value is truncated. This applies app-wide, including Admin > Users 'Email Verified' and 'Created At'. The tooltip is disabled on format-preview samples and where the formatter already renders inside another tooltip (LastActiveDisplay).
1 parent 9782941 commit 1c7cb9d

3 files changed

Lines changed: 122 additions & 44 deletions

File tree

testplanit/app/[locale]/users/profile/[userId]/page.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,46 @@ const UserProfile: React.FC<UserProfileProps> = ({
809809
</div>
810810

811811
<div className="space-y-3">
812+
<div className="flex items-center justify-between">
813+
<span className="text-sm">
814+
{tGlobal(
815+
"home.initialPreferences.dateFormat"
816+
)}
817+
</span>
818+
<Badge variant="secondary">
819+
<DateFormatter
820+
date={sampleDate}
821+
formatString={
822+
user.userPreferences.dateFormat
823+
}
824+
timezone={user.userPreferences.timezone}
825+
tooltip={false}
826+
/>
827+
</Badge>
828+
</div>
829+
830+
<Separator className="opacity-50" />
831+
832+
<div className="flex items-center justify-between">
833+
<span className="text-sm">
834+
{tGlobal(
835+
"home.initialPreferences.timeFormat"
836+
)}
837+
</span>
838+
<Badge variant="secondary">
839+
<DateFormatter
840+
date={sampleDate}
841+
formatString={
842+
user.userPreferences.timeFormat
843+
}
844+
timezone={user.userPreferences.timezone}
845+
tooltip={false}
846+
/>
847+
</Badge>
848+
</div>
849+
850+
<Separator className="opacity-50" />
851+
812852
<div className="flex items-center justify-between">
813853
<span className="text-sm">
814854
{tCommon("fields.timezone")}
@@ -847,7 +887,7 @@ const UserProfile: React.FC<UserProfileProps> = ({
847887
</div>
848888
) : (
849889
<Form {...form}>
850-
<div className="grid gap-4 sm:grid-cols-2 px-0.5">
890+
<div className="grid gap-4 sm:grid-cols-2 px-4">
851891
<FormField
852892
control={form.control}
853893
name="theme"
@@ -1036,6 +1076,7 @@ const UserProfile: React.FC<UserProfileProps> = ({
10361076
session?.user?.preferences
10371077
?.timezone
10381078
}
1079+
tooltip={false}
10391080
/>
10401081
</SelectItem>
10411082
)
@@ -1091,6 +1132,7 @@ const UserProfile: React.FC<UserProfileProps> = ({
10911132
session?.user?.preferences
10921133
?.timezone
10931134
}
1135+
tooltip={false}
10941136
/>
10951137
</SelectItem>
10961138
)
Lines changed: 78 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1+
"use client";
2+
3+
import {
4+
Tooltip,
5+
TooltipContent,
6+
TooltipTrigger,
7+
} from "@/components/ui/tooltip";
18
import { format } from "date-fns";
29
import { formatInTimeZone } from "date-fns-tz";
10+
import { useSession } from "next-auth/react";
311
import { useLocale } from "next-intl";
412
import React from "react";
513
import { getDateFnsLocale } from "~/utils/locales";
@@ -9,15 +17,27 @@ type DateFormatterProps = {
917
date: Date | string | null;
1018
formatString?: string;
1119
timezone?: string | null;
20+
/**
21+
* When true (default), the formatted value is wrapped in a tooltip that
22+
* reveals the full date and time in the viewer's preferred date/time format.
23+
* Useful when the visible value shows only the date (or only the time), or
24+
* is truncated. Set to false for format previews/samples or when the
25+
* formatter is already rendered inside another tooltip/popover.
26+
*/
27+
tooltip?: boolean;
1228
};
1329

30+
const DEFAULT_TOOLTIP_FORMAT = "MM/dd/yyyy hh:mm a";
31+
1432
export const DateFormatter: React.FC<DateFormatterProps> = ({
1533
date,
1634
formatString = "MM-dd-yyyy",
1735
timezone,
36+
tooltip = true,
1837
}) => {
1938
const locale = useLocale();
2039
const dateLocale = getDateFnsLocale(locale);
40+
const { data: session } = useSession();
2141

2242
const dateObject = typeof date === "string" ? new Date(date) : date;
2343
if (
@@ -28,54 +48,69 @@ export const DateFormatter: React.FC<DateFormatterProps> = ({
2848
return null;
2949
}
3050

31-
const finalFormatString = mapDateTimeFormatString(formatString);
32-
33-
let formattedDate: string;
34-
let suffix = "";
35-
36-
try {
37-
if (timezone) {
38-
const ianaTimezone = timezone.replace(/_/g, "/");
39-
formattedDate = formatInTimeZone(
40-
dateObject,
41-
ianaTimezone,
42-
finalFormatString,
43-
{ locale: dateLocale }
44-
);
45-
} else {
46-
formattedDate = format(dateObject, finalFormatString, {
47-
locale: dateLocale,
48-
});
49-
}
50-
} catch (error) {
51-
console.warn(
52-
`Error formatting date with timezone "${timezone}" (IANA: "${timezone?.replace(/_/g, "/")}"):`,
53-
error
54-
);
51+
// Format the date with the given format/timezone, falling back to UTC and
52+
// then local time if the timezone is invalid (mirrors prior behavior).
53+
const formatValue = (rawFormatString: string, tz?: string | null): string => {
54+
const finalFormatString = mapDateTimeFormatString(rawFormatString);
5555
try {
56-
formattedDate = formatInTimeZone(
57-
dateObject,
58-
"Etc/UTC",
59-
finalFormatString,
60-
{ locale: dateLocale }
56+
if (tz) {
57+
return formatInTimeZone(
58+
dateObject,
59+
tz.replace(/_/g, "/"),
60+
finalFormatString,
61+
{ locale: dateLocale }
62+
);
63+
}
64+
return format(dateObject, finalFormatString, { locale: dateLocale });
65+
} catch (error) {
66+
console.warn(
67+
`Error formatting date with timezone "${tz}" (IANA: "${tz?.replace(/_/g, "/")}"):`,
68+
error
6169
);
62-
suffix = " (UTC)";
63-
} catch (utcError) {
64-
console.error(
65-
"Error formatting date with fallback UTC timezone:",
66-
utcError
67-
);
68-
formattedDate = format(dateObject, finalFormatString, {
69-
locale: dateLocale,
70-
});
71-
suffix = " (Local)";
70+
try {
71+
return (
72+
formatInTimeZone(dateObject, "Etc/UTC", finalFormatString, {
73+
locale: dateLocale,
74+
}) + " (UTC)"
75+
);
76+
} catch (utcError) {
77+
console.error(
78+
"Error formatting date with fallback UTC timezone:",
79+
utcError
80+
);
81+
return (
82+
format(dateObject, finalFormatString, { locale: dateLocale }) +
83+
" (Local)"
84+
);
85+
}
7286
}
87+
};
88+
89+
const formattedDate = formatValue(formatString, timezone);
90+
91+
if (!tooltip) {
92+
return <>{formattedDate}</>;
7393
}
7494

95+
// Tooltip shows the full date + time in the viewer's preferred format.
96+
const preferredDateFormat = session?.user?.preferences?.dateFormat;
97+
const preferredTimeFormat = session?.user?.preferences?.timeFormat;
98+
const preferredTimezone = session?.user?.preferences?.timezone;
99+
const tooltipFormatString =
100+
preferredDateFormat && preferredTimeFormat
101+
? `${preferredDateFormat} ${preferredTimeFormat}`
102+
: DEFAULT_TOOLTIP_FORMAT;
103+
const tooltipText = formatValue(
104+
tooltipFormatString,
105+
timezone ?? preferredTimezone
106+
);
107+
75108
return (
76-
<>
77-
{formattedDate}
78-
{suffix}
79-
</>
109+
<Tooltip>
110+
<TooltipTrigger asChild>
111+
<span>{formattedDate}</span>
112+
</TooltipTrigger>
113+
<TooltipContent>{tooltipText}</TooltipContent>
114+
</Tooltip>
80115
);
81116
};

testplanit/components/LastActiveDisplay.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const LastActiveDisplay: React.FC<LastActiveDisplayProps> = ({
5656
date={dateObj}
5757
formatString={formatString}
5858
timezone={preferredTimezone}
59+
tooltip={false}
5960
/>
6061
</TooltipContent>
6162
</Tooltip>

0 commit comments

Comments
 (0)