mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
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:
parent
676ad5b107
commit
3b63c5ea1a
2 changed files with 80 additions and 9 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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've joined {acceptedInfo?.portfolioName}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
You now have access. Head over when you'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue