|
1 | 1 | import { useForm } from "@conform-to/react"; |
2 | 2 | import { parse } from "@conform-to/zod"; |
3 | 3 | 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"; |
5 | 5 | import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; |
6 | | -import { useState } from "react"; |
| 6 | +import { useEffect, useRef, useState } from "react"; |
7 | 7 | import { type UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson"; |
8 | 8 | import invariant from "tiny-invariant"; |
9 | 9 | import { z } from "zod"; |
@@ -374,12 +374,61 @@ function LeaveTeamModal({ |
374 | 374 | ); |
375 | 375 | } |
376 | 376 |
|
| 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 | + |
377 | 385 | 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 | + |
378 | 423 | return ( |
379 | 424 | <Form method="post" action={resendInvitePath()} className="flex"> |
380 | 425 | <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"} |
383 | 432 | </Button> |
384 | 433 | </Form> |
385 | 434 | ); |
|
0 commit comments