Skip to content

Commit c76086a

Browse files
committed
Add a cooldown on the Resend invite button
1 parent cee54f5 commit c76086a

File tree

1 file changed

+53
-4
lines changed
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team

1 file changed

+53
-4
lines changed

apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { useForm } from "@conform-to/react";
22
import { parse } from "@conform-to/zod";
33
import { EnvelopeIcon, NoSymbolIcon, UserPlusIcon } from "@heroicons/react/20/solid";
4-
import { Form, type MetaFunction, useActionData } from "@remix-run/react";
4+
import { Form, type MetaFunction, useActionData, useNavigation } from "@remix-run/react";
55
import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
6-
import { useState } from "react";
6+
import { useEffect, useRef, useState } from "react";
77
import { type UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson";
88
import invariant from "tiny-invariant";
99
import { z } from "zod";
@@ -374,12 +374,61 @@ function LeaveTeamModal({
374374
);
375375
}
376376

377+
const RESEND_COOLDOWN_SECONDS = 30;
378+
379+
function initialCooldown(updatedAt: Date | string): number {
380+
const elapsed = Math.floor((Date.now() - new Date(updatedAt).getTime()) / 1000);
381+
const remaining = RESEND_COOLDOWN_SECONDS - elapsed;
382+
return remaining > 0 ? remaining : 0;
383+
}
384+
377385
function ResendButton({ invite }: { invite: Invite }) {
386+
const navigation = useNavigation();
387+
const isSubmitting =
388+
navigation.state === "submitting" &&
389+
navigation.formAction === resendInvitePath() &&
390+
navigation.formData?.get("inviteId") === invite.id;
391+
const prevSubmitting = useRef(false);
392+
const [cooldown, setCooldown] = useState(() => initialCooldown(invite.updatedAt));
393+
const intervalRef = useRef<ReturnType<typeof setInterval>>();
394+
395+
useEffect(() => {
396+
if (prevSubmitting.current && !isSubmitting) {
397+
setCooldown(RESEND_COOLDOWN_SECONDS);
398+
}
399+
prevSubmitting.current = isSubmitting;
400+
}, [isSubmitting]);
401+
402+
useEffect(() => {
403+
if (cooldown <= 0) {
404+
clearInterval(intervalRef.current);
405+
return;
406+
}
407+
408+
intervalRef.current = setInterval(() => {
409+
setCooldown((c) => {
410+
if (c <= 1) {
411+
clearInterval(intervalRef.current);
412+
return 0;
413+
}
414+
return c - 1;
415+
});
416+
}, 1000);
417+
418+
return () => clearInterval(intervalRef.current);
419+
}, [cooldown > 0]); // only re-run when transitioning between active/inactive
420+
421+
const isDisabled = isSubmitting || cooldown > 0;
422+
378423
return (
379424
<Form method="post" action={resendInvitePath()} className="flex">
380425
<input type="hidden" value={invite.id} name="inviteId" />
381-
<Button type="submit" variant="secondary/small">
382-
Resend invite
426+
<Button type="submit" variant="secondary/small" disabled={isDisabled}>
427+
{isSubmitting
428+
? "Sending…"
429+
: cooldown > 0
430+
? <span className="tabular-nums">{`Sent – resend in ${cooldown}s`}</span>
431+
: "Resend invite"}
383432
</Button>
384433
</Form>
385434
);

0 commit comments

Comments
 (0)