Confirm invitation accept with a dialog + refresh portfolios list

Replaces the auto-dismiss toast on accept with a shadcn Dialog that
names the portfolio and offers a "Go to {portfolioName}" CTA. Decline
keeps the existing toast. router.refresh() updates /home in place, and
revalidatePath("/home") in the API handler guarantees the server-rendered
portfolio list is fresh on any later navigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-28 10:55:34 +00:00
parent 676ad5b107
commit 3b63c5ea1a
2 changed files with 80 additions and 9 deletions

View file

@ -1,5 +1,6 @@
import { db } from "@/app/db/db";
import { NextRequest, NextResponse } from "next/server";
import { revalidatePath } from "next/cache";
import {
portfolio,
portfolioInvitations,
@ -166,6 +167,11 @@ export async function POST(req: NextRequest) {
newMembership: plan.memberships.length > 0,
});
// /home renders the user's portfolio list from the DB in a server
// component; invalidate so the next navigation there picks up the new
// membership. router.refresh() handles the in-place case client-side.
revalidatePath("/home");
return NextResponse.json(
{
success: true,

View file

@ -1,11 +1,22 @@
"use client";
import { useState } from "react";
import { Menu } from "@headlessui/react";
import { signOut, useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useToast } from "@/app/hooks/use-toast";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/app/shadcn_components/ui/dialog";
import { Button } from "@/app/shadcn_components/ui/button";
type PendingInvitation = {
invitationId: string;
@ -49,6 +60,11 @@ function ProfileDropDown({ userImage }: { userImage: string }) {
const queryClient = useQueryClient();
const { toast } = useToast();
const router = useRouter();
const [acceptedInfo, setAcceptedInfo] = useState<{
portfolioId: string;
portfolioName: string;
} | null>(null);
const { data: invitations = [], isLoading } = useQuery({
queryKey: INVITATIONS_KEY,
@ -87,16 +103,30 @@ function ProfileDropDown({ userImage }: { userImage: string }) {
variant: "destructive",
});
},
onSuccess: (_data, vars) => {
onSuccess: (data, vars) => {
const inv = invitations.find((i) => i.invitationId === vars.invitationId);
const portfolioLabel = inv ? `the ${inv.portfolioName} portfolio` : "the portfolio";
toast({
title: vars.action === "accept" ? "Joined portfolio" : "Invitation declined",
description:
vars.action === "accept"
? `You now have access to ${portfolioLabel}.`
: `You've declined the invitation to ${portfolioLabel}.`,
});
if (vars.action === "accept") {
const portfolioId = data?.portfolioId ?? inv?.portfolioId;
const portfolioName = inv?.portfolioName ?? "the portfolio";
// /home's server-rendered portfolio list won't pick up the new
// membership without an explicit refresh; the API handler also calls
// revalidatePath("/home") so any later navigation is fresh too.
router.refresh();
if (portfolioId) {
setAcceptedInfo({ portfolioId, portfolioName });
} else {
toast({
title: "Joined portfolio",
description: `You now have access to ${portfolioName}.`,
});
}
} else {
const portfolioLabel = inv ? `the ${inv.portfolioName} portfolio` : "the portfolio";
toast({
title: "Invitation declined",
description: `You've declined the invitation to ${portfolioLabel}.`,
});
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: INVITATIONS_KEY });
@ -104,6 +134,7 @@ function ProfileDropDown({ userImage }: { userImage: string }) {
});
return (
<>
<Menu as="div" className="relative">
<Menu.Button className="rounded-full relative">
{userImage ? (
@ -242,6 +273,40 @@ function ProfileDropDown({ userImage }: { userImage: string }) {
</Menu.Item>
</Menu.Items>
</Menu>
<Dialog
open={!!acceptedInfo}
onOpenChange={(open) => {
if (!open) setAcceptedInfo(null);
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>
You&apos;ve joined {acceptedInfo?.portfolioName}
</DialogTitle>
<DialogDescription>
You now have access. Head over when you&apos;re ready.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setAcceptedInfo(null)}>
Close
</Button>
<Button
onClick={() => {
if (acceptedInfo) {
router.push(`/portfolio/${acceptedInfo.portfolioId}`);
}
setAcceptedInfo(null);
}}
>
Go to {acceptedInfo?.portfolioName ?? "portfolio"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}