Add a time component (#2347)

* Add Time component

* Use the Time component on the tournament and scrims pages

* fix formatting issues

* renamed the Time component to TimePopover

* change the Time component trigger to a button

* remove unused import

* fix button styling
This commit is contained in:
Phil-hacker 2025-06-01 08:25:35 +02:00 committed by GitHub
parent 1c26ff31c8
commit 2fd3ff1240
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 120 additions and 15 deletions

View File

@ -0,0 +1,83 @@
import { useRef, useState } from "react";
import * as React from "react";
import { Dialog } from "react-aria-components";
import { Popover } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { useCopyToClipboard } from "react-use";
import { SendouButton } from "./elements/Button";
import { CheckmarkIcon } from "./icons/Checkmark";
import { ClipboardIcon } from "./icons/Clipboard";
export default function TimePopover({
time,
options = {
minute: "numeric",
hour: "numeric",
day: "numeric",
month: "long",
},
}: {
time: Date;
options?: Intl.DateTimeFormatOptions;
}) {
const { i18n } = useTranslation();
const [open, setOpen] = useState(false);
const triggerRef = useRef(null);
const { t } = useTranslation(["common"]);
const [state, copyToClipboard] = useCopyToClipboard();
const [copySuccess, setCopySuccess] = React.useState(false);
React.useEffect(() => {
if (!state.value) return;
setCopySuccess(true);
const timeout = setTimeout(() => setCopySuccess(false), 2000);
return () => clearTimeout(timeout);
}, [state]);
return (
<div>
<button
type="button"
ref={triggerRef}
className="dotted clickable text-only-button"
onClick={() => {
setOpen(true);
}}
>
{time.toLocaleString(i18n.language, options)}
</button>
<Popover
isOpen={open}
className={"sendou-popover-content"}
onOpenChange={setOpen}
triggerRef={triggerRef}
>
<Dialog>
<div className="stack sm">
<div className="text-center">
{time.toLocaleTimeString(i18n.language, {
timeZoneName: "long",
hour: "numeric",
minute: "numeric",
})}
</div>
<SendouButton
size="miniscule"
variant="minimal"
onPress={() => copyToClipboard(`<t:${time.valueOf() / 1000}:F>`)}
icon={copySuccess ? <CheckmarkIcon /> : <ClipboardIcon />}
>
{t("common:actions.copyTimestampForDiscord")}
</SendouButton>
</div>
</Dialog>
</Popover>
</div>
);
}

View File

@ -15,6 +15,7 @@ import type { ScrimPost as ScrimPostType } from "../scrims-types";
import { loader } from "../loaders/scrims.$id.server";
export { loader };
import TimePopover from "~/components/TimePopover";
import styles from "./scrims.$id.module.css";
export const handle: SendouRouteHandle = {
@ -50,19 +51,21 @@ export default function ScrimPage() {
function ScrimHeader() {
const { t } = useTranslation(["scrims"]);
const data = useLoaderData<typeof loader>();
const { i18n } = useTranslation();
return (
<div className="line-height-tight" data-testid="match-header">
<h2 className="text-lg" suppressHydrationWarning>
{databaseTimestampToDate(data.post.at).toLocaleString(i18n.language, {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
})}
<TimePopover
time={databaseTimestampToDate(data.post.at)}
options={{
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
}}
/>
</h2>
<div className="text-lighter text-xs font-bold">
{t("scrims:page.scheduledScrim")}

View File

@ -59,6 +59,7 @@ import {
} from "../tournament-utils";
import { useTournament } from "./to.$id";
import TimePopover from "~/components/TimePopover";
import { action } from "../actions/to.$id.register.server";
import { loader } from "../loaders/to.$id.register.server";
export { loader, action };
@ -66,7 +67,6 @@ export { loader, action };
export default function TournamentRegisterPage() {
const user = useUser();
const isMounted = useIsMounted();
const { i18n } = useTranslation();
const tournament = useTournament();
const startsAtEvenHour = tournament.ctx.startTime.getMinutes() === 0;
@ -123,15 +123,17 @@ export default function TournamentRegisterPage() {
<div className="tournament__by mt-2">
<div className="stack horizontal xs items-center">
<ClockIcon className="tournament__info__icon" />{" "}
{isMounted
? tournament.ctx.startTime.toLocaleString(i18n.language, {
timeZoneName: "short",
{isMounted ? (
<TimePopover
time={tournament.ctx.startTime}
options={{
minute: startsAtEvenHour ? undefined : "numeric",
hour: "numeric",
day: "numeric",
month: "long",
})
: null}
}}
/>
) : null}
</div>
</div>
) : null}

View File

@ -252,6 +252,21 @@ details summary {
user-select: none;
}
.text-only-button {
cursor: pointer;
border: 0;
background-color: inherit;
color: inherit;
margin: 0;
padding: 0;
}
.dotted {
text-decoration-style: dotted;
text-decoration-line: underline;
text-decoration-thickness: 2px;
}
.summary {
border-radius: var(--rounded);
background-color: var(--bg-darker-transparent);

View File

@ -99,6 +99,7 @@
"actions.showMore": "",
"actions.showLess": "",
"actions.copyToClipboard": "In die Zwischenablage kopieren",
"actions.copyTimestampForDiscord": "Discord-Zeitstempel kopieren",
"actions.create": "Erstellen",
"actions.close": "Schließen",
"actions.cancel": "Abbrechen",

View File

@ -99,6 +99,7 @@
"actions.showMore": "Show more",
"actions.showLess": "Show less",
"actions.copyToClipboard": "Copy to clipboard",
"actions.copyTimestampForDiscord": "Copy Discord timestamp",
"actions.create": "Create",
"actions.close": "Close",
"actions.cancel": "Cancel",