Skip to content

Commit cb5bbaf

Browse files
committed
Makes the copy function into a hook and separate button + adds copy dateTime button to the date tooltip
1 parent f1638ab commit cb5bbaf

File tree

5 files changed

+161
-97
lines changed

5 files changed

+161
-97
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: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
2+
import { cn } from "~/utils/cn";
3+
import { useCopy } from "~/hooks/useCopy";
4+
import { Button } from "./Buttons";
5+
import { SimpleTooltip } from "./Tooltip";
6+
7+
type CopyButtonProps = {
8+
value: string;
9+
variant?: "icon" | "button";
10+
size?: "small" | "medium";
11+
className?: string;
12+
buttonClassName?: string;
13+
showTooltip?: boolean;
14+
buttonVariant?: "primary" | "secondary" | "tertiary" | "minimal";
15+
};
16+
17+
export function CopyButton({
18+
value,
19+
variant = "button",
20+
size = "medium",
21+
className,
22+
buttonClassName,
23+
showTooltip = true,
24+
buttonVariant = "tertiary",
25+
}: CopyButtonProps) {
26+
const { copy, copied } = useCopy(value);
27+
28+
const iconSize = size === "small" ? "size-3.5" : "size-4";
29+
const buttonSize = size === "small" ? "h-6" : "h-8";
30+
31+
const button =
32+
variant === "icon" ? (
33+
<span
34+
onClick={copy}
35+
className={cn(
36+
buttonSize,
37+
"flex items-center justify-center rounded border border-charcoal-650 bg-charcoal-750 px-1.5",
38+
copied
39+
? "text-green-500"
40+
: "text-text-dimmed hover:border-charcoal-600 hover:bg-charcoal-700 hover:text-text-bright",
41+
buttonClassName
42+
)}
43+
>
44+
{copied ? (
45+
<ClipboardCheckIcon className={iconSize} />
46+
) : (
47+
<ClipboardIcon className={iconSize} />
48+
)}
49+
</span>
50+
) : (
51+
<Button
52+
variant={`${buttonVariant}/${size}`}
53+
onClick={copy}
54+
className={cn("shrink-0", buttonClassName)}
55+
>
56+
{copied ? (
57+
<ClipboardCheckIcon
58+
className={cn(
59+
iconSize,
60+
buttonVariant === "primary" ? "text-background-dimmed" : "text-green-500"
61+
)}
62+
/>
63+
) : (
64+
<ClipboardIcon
65+
className={cn(
66+
iconSize,
67+
buttonVariant === "primary" ? "text-background-dimmed" : "text-text-dimmed"
68+
)}
69+
/>
70+
)}
71+
</Button>
72+
);
73+
74+
if (!showTooltip) return <span className={className}>{button}</span>;
75+
76+
return (
77+
<span className={className}>
78+
<SimpleTooltip
79+
button={button}
80+
content={copied ? "Copied!" : "Copy"}
81+
className="font-sans"
82+
disableHoverableContent
83+
/>
84+
</span>
85+
);
86+
}

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

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,12 @@
1-
import { useCallback, useState } from "react";
1+
import { useState } from "react";
22
import { SimpleTooltip } from "~/components/primitives/Tooltip";
33
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
44
import { cn } from "~/utils/cn";
5+
import { useCopy } from "~/hooks/useCopy";
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

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Fragment, type ReactNode, useEffect, useState } from "react";
44
import { useLocales } from "./LocaleProvider";
55
import { Paragraph } from "./Paragraph";
66
import { SimpleTooltip } from "./Tooltip";
7+
import { CopyButton } from "./CopyButton";
78

89
type DateTimeProps = {
910
date: Date | string;
@@ -38,11 +39,13 @@ export const DateTime = ({
3839
<DateTimeTooltipContent
3940
title="UTC"
4041
dateTime={formatDateTime(realDate, "UTC", locales, true, true)}
42+
isoDateTime={formatDateTimeISO(realDate, "UTC")}
4143
icon={<GlobeAltIcon className="size-4 text-blue-500" />}
4244
/>
4345
<DateTimeTooltipContent
4446
title="Local"
4547
dateTime={formatDateTime(realDate, localTimeZone, locales, true, true)}
48+
isoDateTime={formatDateTimeISO(realDate, localTimeZone)}
4649
icon={<Laptop className="size-4 text-green-500" />}
4750
/>
4851
</div>
@@ -51,16 +54,19 @@ export const DateTime = ({
5154
<DateTimeTooltipContent
5255
title={timeZone}
5356
dateTime={formatDateTime(realDate, timeZone, locales, true, true)}
57+
isoDateTime={formatDateTimeISO(realDate, timeZone)}
5458
icon={<GlobeAmericasIcon className="size-4 text-purple-500" />}
5559
/>
5660
<DateTimeTooltipContent
5761
title="UTC"
5862
dateTime={formatDateTime(realDate, "UTC", locales, true, true)}
63+
isoDateTime={formatDateTimeISO(realDate, "UTC")}
5964
icon={<GlobeAltIcon className="size-4 text-blue-500" />}
6065
/>
6166
<DateTimeTooltipContent
6267
title="Local"
6368
dateTime={formatDateTime(realDate, localTimeZone, locales, true, true)}
69+
isoDateTime={formatDateTimeISO(realDate, localTimeZone)}
6470
icon={<Laptop className="size-4 text-green-500" />}
6571
/>
6672
</div>
@@ -84,7 +90,6 @@ export const DateTime = ({
8490
}
8591
content={tooltipContent}
8692
side="right"
87-
// disableHoverableContent
8893
/>
8994
);
9095
};
@@ -107,6 +112,10 @@ export function formatDateTime(
107112
}).format(date);
108113
}
109114

115+
export function formatDateTimeISO(date: Date, timeZone: string): string {
116+
return new Date(date.toLocaleString("en-US", { timeZone })).toISOString();
117+
}
118+
110119
// New component that only shows date when it changes
111120
export const SmartDateTime = ({ date, previousDate = null, timeZone = "UTC" }: DateTimeProps) => {
112121
const locales = useLocales();
@@ -266,19 +275,28 @@ function formatDateTimeShort(date: Date, timeZone: string, locales: string[]): s
266275
type DateTimeTooltipContentProps = {
267276
title: string;
268277
dateTime: string;
278+
isoDateTime: string;
269279
icon: ReactNode;
270280
};
271281

272-
function DateTimeTooltipContent({ title, dateTime, icon }: DateTimeTooltipContentProps) {
282+
function DateTimeTooltipContent({
283+
title,
284+
dateTime,
285+
isoDateTime,
286+
icon,
287+
}: DateTimeTooltipContentProps) {
273288
return (
274289
<div className="flex flex-col gap-1">
275290
<div className="flex items-center gap-1 text-sm">
276291
{icon}
277292
<span className="font-medium">{title}</span>
278293
</div>
279-
<Paragraph variant="extra-small" className="text-text-dimmed">
280-
{dateTime}
281-
</Paragraph>
294+
<div className="flex items-center gap-2">
295+
<Paragraph variant="extra-small" className="text-text-dimmed">
296+
{dateTime}
297+
</Paragraph>
298+
<CopyButton value={isoDateTime} variant="icon" size="small" showTooltip={false} />
299+
</div>
282300
</div>
283301
);
284302
}

0 commit comments

Comments
 (0)