Skip to content

Commit 553a035

Browse files
authored
Show a tooltip when hovering over any DateTime (#2003)
* Adds a tooltip to display the UTC, timezone and local time for DateTime * Made it safer for SSR * Makes the copy function into a hook and separate button + adds copy dateTime button to the date tooltip * Adds an extra-small size copy button * Show the offset (UTC +1) next to your local time * Optionally don’t show the date tooltip. Defaults to true (always show) * Tidy imports * Refactor the tooltip content to remove duplicated markup * Coderabbit suggestion to include support for minutes for some timezones like Nepal
1 parent af01947 commit 553a035

File tree

6 files changed

+246
-117
lines changed

6 files changed

+246
-117
lines changed
Lines changed: 27 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,62 @@
1-
import { CheckIcon } from "@heroicons/react/20/solid";
2-
import { useCallback, useEffect, useRef, useState } from "react";
1+
import { useEffect, useRef, useState } from "react";
32
import { cn } from "~/utils/cn";
4-
import { Button } from "./Buttons";
5-
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
3+
import { CopyButton } from "./CopyButton";
64

75
const variants = {
86
"primary/small": {
97
container:
108
"flex items-center text-text-dimmed font-mono rounded border bg-charcoal-750 text-xs transition hover:bg-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
119
input:
1210
"bg-transparent border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
13-
buttonVariant: "primary/small" as const,
11+
buttonVariant: "primary" as const,
12+
size: "small" as const,
1413
button: "rounded-l-none",
15-
iconSize: "h-3 w-3",
16-
iconPadding: "pl-1",
1714
},
1815
"secondary/small": {
1916
container:
2017
"flex items-center text-text-dimmed font-mono rounded border bg-charcoal-750 text-xs transition hover:bg-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
2118
input:
2219
"bg-transparent border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
23-
buttonVariant: "tertiary/small" as const,
20+
buttonVariant: "tertiary" as const,
21+
size: "small" as const,
2422
button: "rounded-l-none border-l border-charcoal-750",
25-
iconSize: "h-3 w-3",
26-
iconPadding: "pl-1",
2723
},
2824
"tertiary/small": {
2925
container:
3026
"group/clipboard flex items-center text-text-dimmed font-mono rounded bg-transparent border border-transparent text-xs transition duration-150 hover:border-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
3127
input:
3228
"bg-transparent border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
33-
buttonVariant: "minimal/small" as const,
29+
buttonVariant: "minimal" as const,
30+
size: "small" as const,
3431
button:
3532
"rounded-l-none border-l border-transparent transition group-hover/clipboard:border-charcoal-700",
36-
iconSize: "h-3 w-3",
37-
iconPadding: "pl-1",
3833
},
3934
"primary/medium": {
4035
container:
4136
"flex items-center text-text-dimmed font-mono rounded border bg-charcoal-750 text-sm transition hover:bg-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
4237
input:
4338
"bg-transparent border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
44-
buttonVariant: "primary/medium" as const,
39+
buttonVariant: "primary" as const,
40+
size: "medium" as const,
4541
button: "rounded-l-none",
46-
iconSize: "h-4 w-4",
47-
iconPadding: "pl-2",
4842
},
4943
"secondary/medium": {
5044
container:
5145
"flex items-center text-text-dimmed font-mono rounded bg-charcoal-750 text-sm transition hover:bg-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
5246
input:
5347
"bg-transparent border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
54-
buttonVariant: "tertiary/medium" as const,
48+
buttonVariant: "tertiary" as const,
49+
size: "medium" as const,
5550
button: "rounded-l-none border-l border-charcoal-750",
56-
iconSize: "h-4 w-4",
57-
iconPadding: "pl-2",
5851
},
5952
"tertiary/medium": {
6053
container:
6154
"group flex items-center text-text-dimmed font-mono rounded bg-transparent border border-transparent text-sm transition hover:border-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
6255
input:
6356
"bg-transparent border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
64-
buttonVariant: "minimal/medium" as const,
57+
buttonVariant: "minimal" as const,
58+
size: "medium" as const,
6559
button: "rounded-l-none border-l border-transparent transition group-hover:border-charcoal-700",
66-
iconSize: "h-4 w-4",
67-
iconPadding: "pl-2",
6860
},
6961
};
7062

@@ -88,36 +80,19 @@ export function ClipboardField({
8880
fullWidth = true,
8981
}: ClipboardFieldProps) {
9082
const [isSecure, setIsSecure] = useState(secure !== undefined && secure);
91-
const [copied, setCopied] = useState(false);
92-
93-
const copy = useCallback(
94-
(event: React.MouseEvent<HTMLButtonElement>) => {
95-
event.preventDefault();
96-
event.stopPropagation();
97-
navigator.clipboard.writeText(value);
98-
setCopied(true);
99-
setTimeout(() => {
100-
setCopied(false);
101-
}, 1500);
102-
},
103-
[value]
104-
);
83+
const inputIcon = useRef<HTMLInputElement>(null);
84+
const { container, input, buttonVariant, button, size } = variants[variant];
10585

10686
useEffect(() => {
10787
setIsSecure(secure !== undefined && secure);
10888
}, [secure]);
10989

110-
const { container, input, buttonVariant, button } = variants[variant];
111-
const iconClassName = variants[variant].iconSize;
112-
const iconPosition = variants[variant].iconPadding;
113-
const inputIcon = useRef<HTMLInputElement>(null);
114-
11590
return (
11691
<span className={cn(container, fullWidth ? "w-full" : "max-w-fit", className)}>
11792
{icon && (
11893
<span
11994
onClick={() => inputIcon.current && inputIcon.current.focus()}
120-
className={cn(iconPosition, "flex items-center")}
95+
className="flex items-center pl-1"
12196
>
12297
{icon}
12398
</span>
@@ -132,51 +107,26 @@ export function ClipboardField({
132107
fullWidth ? "w-full" : "max-w-fit",
133108
input
134109
)}
135-
// size={value.length}
136-
// maxLength={3}
137110
onFocus={(e) => {
138111
if (secure) {
139-
setIsSecure((i) => false);
112+
setIsSecure(false);
140113
}
141114
e.currentTarget.select();
142115
}}
143116
onBlur={() => {
144117
if (secure) {
145-
setIsSecure((i) => true);
118+
setIsSecure(true);
146119
}
147120
}}
148121
/>
149-
{iconButton ? (
150-
<Button
151-
variant={buttonVariant}
152-
onClick={copy}
153-
className={cn("shrink grow-0 px-1.5", button)}
154-
>
155-
{copied ? (
156-
<ClipboardCheckIcon
157-
className={cn(
158-
"h-4 w-4",
159-
buttonVariant === "primary/small" || buttonVariant === "primary/medium"
160-
? "text-background-dimmed"
161-
: "text-green-500"
162-
)}
163-
/>
164-
) : (
165-
<ClipboardIcon
166-
className={cn(
167-
"h-4 w-4",
168-
buttonVariant === "primary/small" || buttonVariant === "primary/medium"
169-
? "text-background-dimmed"
170-
: "text-text-dimmed"
171-
)}
172-
/>
173-
)}
174-
</Button>
175-
) : (
176-
<Button variant={buttonVariant} onClick={copy} className={cn("shrink-0 grow-0", button)}>
177-
{copied ? <CheckIcon className="mx-[0.4rem] h-4 w-4 text-green-500" /> : "Copy"}
178-
</Button>
179-
)}
122+
<CopyButton
123+
value={value}
124+
variant={iconButton ? "icon" : "button"}
125+
buttonVariant={buttonVariant}
126+
size={size}
127+
buttonClassName={button}
128+
showTooltip={false}
129+
/>
180130
</span>
181131
);
182132
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
2+
import { useCopy } from "~/hooks/useCopy";
3+
import { cn } from "~/utils/cn";
4+
import { Button } from "./Buttons";
5+
import { SimpleTooltip } from "./Tooltip";
6+
7+
const sizes = {
8+
"extra-small": {
9+
icon: "size-3",
10+
button: "h-5 px-1",
11+
},
12+
small: {
13+
icon: "size-3.5",
14+
button: "h-6 px-1",
15+
},
16+
medium: {
17+
icon: "size-4",
18+
button: "h-8 px-1.5",
19+
},
20+
};
21+
22+
type CopyButtonProps = {
23+
value: string;
24+
variant?: "icon" | "button";
25+
size?: keyof typeof sizes;
26+
className?: string;
27+
buttonClassName?: string;
28+
showTooltip?: boolean;
29+
buttonVariant?: "primary" | "secondary" | "tertiary" | "minimal";
30+
};
31+
32+
export function CopyButton({
33+
value,
34+
variant = "button",
35+
size = "medium",
36+
className,
37+
buttonClassName,
38+
showTooltip = true,
39+
buttonVariant = "tertiary",
40+
}: CopyButtonProps) {
41+
const { copy, copied } = useCopy(value);
42+
43+
const { icon: iconSize, button: buttonSize } = sizes[size];
44+
45+
const button =
46+
variant === "icon" ? (
47+
<span
48+
onClick={copy}
49+
className={cn(
50+
buttonSize,
51+
"flex items-center justify-center rounded border border-charcoal-650 bg-charcoal-750",
52+
copied
53+
? "text-green-500"
54+
: "text-text-dimmed hover:border-charcoal-600 hover:bg-charcoal-700 hover:text-text-bright",
55+
buttonClassName
56+
)}
57+
>
58+
{copied ? (
59+
<ClipboardCheckIcon className={iconSize} />
60+
) : (
61+
<ClipboardIcon className={iconSize} />
62+
)}
63+
</span>
64+
) : (
65+
<Button
66+
variant={`${buttonVariant}/${size === "extra-small" ? "small" : size}`}
67+
onClick={copy}
68+
className={cn("shrink-0", buttonClassName)}
69+
>
70+
{copied ? (
71+
<ClipboardCheckIcon
72+
className={cn(
73+
iconSize,
74+
buttonVariant === "primary" ? "text-background-dimmed" : "text-green-500"
75+
)}
76+
/>
77+
) : (
78+
<ClipboardIcon
79+
className={cn(
80+
iconSize,
81+
buttonVariant === "primary" ? "text-background-dimmed" : "text-text-dimmed"
82+
)}
83+
/>
84+
)}
85+
</Button>
86+
);
87+
88+
if (!showTooltip) return <span className={className}>{button}</span>;
89+
90+
return (
91+
<span className={className}>
92+
<SimpleTooltip
93+
button={button}
94+
content={copied ? "Copied!" : "Copy"}
95+
className="font-sans"
96+
disableHoverableContent
97+
/>
98+
</span>
99+
);
100+
}

apps/webapp/app/components/primitives/CopyableText.tsx

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,12 @@
1-
import { useCallback, useState } from "react";
2-
import { SimpleTooltip } from "~/components/primitives/Tooltip";
31
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
2+
import { useState } from "react";
3+
import { SimpleTooltip } from "~/components/primitives/Tooltip";
4+
import { useCopy } from "~/hooks/useCopy";
45
import { cn } from "~/utils/cn";
56

67
export function CopyableText({ value, className }: { value: string; className?: string }) {
78
const [isHovered, setIsHovered] = useState(false);
8-
const [copied, setCopied] = useState(false);
9-
10-
const copy = useCallback(
11-
(e: React.MouseEvent) => {
12-
e.preventDefault();
13-
e.stopPropagation();
14-
navigator.clipboard.writeText(value);
15-
setCopied(true);
16-
setTimeout(() => {
17-
setCopied(false);
18-
}, 1500);
19-
},
20-
[value]
21-
);
9+
const { copy, copied } = useCopy(value);
2210

2311
return (
2412
<span

0 commit comments

Comments
 (0)