Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 18 additions & 15 deletions src/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ReactQueryProvider } from "@/components/providers/ReactQueryProvider";
import InstructionPanel from "@/components/auth/InstructionPanel";
import Image from "next/image";

Expand All @@ -7,23 +8,25 @@ export default function AuthLayout({
children: React.ReactNode;
}) {
return (
<div className="flex h-screen">
<div className="flex w-[60%] flex-col">
<div className="w-full p-5">
<Image
src="/InfraJuno.png"
alt="Juno"
width={180}
height={30}
className="mb-10"
/>
<ReactQueryProvider>
<div className="flex h-screen">
<div className="flex w-[60%] flex-col">
<div className="w-full p-5">
<Image
src="/InfraJuno.png"
alt="Juno"
width={180}
height={30}
className="mb-10"
/>
</div>
<div className="flex flex-col items-center">{children}</div>
</div>
<div className="flex flex-col items-center">{children}</div>
</div>

<div className="relative w-[40%] overflow-visible bg-neutral-950">
<InstructionPanel />
<div className="relative w-[40%] overflow-visible bg-neutral-950">
<InstructionPanel />
</div>
</div>
</div>
</ReactQueryProvider>
);
}
80 changes: 42 additions & 38 deletions src/app/(auth)/request-account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/auth/ui";
import { Alert } from "@/components/ui/alert";
import {
Form,
FormControl,
Expand All @@ -19,11 +18,14 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { requestNewAccount } from "@/lib/accountRequests";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckCircle2, CircleX, Loader2 } from "lucide-react";
import { useMutation } from "@tanstack/react-query";
import { CheckCircle2, Loader2 } from "lucide-react";
import Link from "next/link";
import React, { useState } from "react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

const requestAccountSchema = z
Expand Down Expand Up @@ -52,9 +54,7 @@ const requestAccountSchema = z
type RequestAccountValues = z.infer<typeof requestAccountSchema>;

const RequestAccountPage = () => {
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);

const form = useForm<RequestAccountValues>({
resolver: zodResolver(requestAccountSchema),
Expand All @@ -69,29 +69,30 @@ const RequestAccountPage = () => {

const userType = form.watch("userType");

async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const requestAccountMutation = useMutation({
mutationFn: async (values: RequestAccountValues) => {
const result = await requestNewAccount({
name: values.name,
email: values.email,
password: values.password,
userType: values.userType === "Admin" ? "ADMIN" : "USER",
projectName:
values.userType === "Admin" ? values.projectName : undefined,
});

const valid = await form.trigger();
if (!valid) return;

setLoading(true);
setError("");

/* TODO: Send request to infra team for approval
const values = form.getValues();
const payload = {
name: values.name,
email: values.email,
password: values.password,
userType: values.userType,
projectName: values.userType === "Admin" ? values.projectName : undefined,
};
*/
if (!result.success) {
throw new Error(result.error || "Failed to request account.");
}

setSuccess(true);
setLoading(false);
}
return result;
},
onSuccess: () => setSuccess(true),
onError: (error) => {
toast.error("Failed to request account", {
description: error.message,
});
},
});

if (success) {
return (
Expand Down Expand Up @@ -120,17 +121,14 @@ const RequestAccountPage = () => {
Request new account
</h2>

{error.length > 0 && (
<Alert className="mb-4 border-red-500/20 bg-red-500/10">
<div className="flex items-center gap-2 text-red-400">
<CircleX className="h-4 w-4" />
<span className="text-sm">{error}</span>
</div>
</Alert>
)}

<Form {...form}>
<form onSubmit={handleSubmit} noValidate className="space-y-4">
<form
onSubmit={form.handleSubmit((values) =>
requestAccountMutation.mutate(values),
)}
noValidate
className="space-y-4"
>
<FormField
control={form.control}
name="name"
Expand Down Expand Up @@ -233,8 +231,14 @@ const RequestAccountPage = () => {
/>
)}

<Button type="submit" className="mt-2 w-full" disabled={loading}>
{loading && <Loader2 className="animate-spin" />}
<Button
type="submit"
className="mt-2 w-full"
disabled={requestAccountMutation.isPending}
>
{requestAccountMutation.isPending && (
<Loader2 className="animate-spin" />
)}
Request Account
</Button>
</form>
Expand Down
46 changes: 35 additions & 11 deletions src/components/admin/PendingAccountRequests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { CheckCircle, XCircle } from "lucide-react";
import { toast } from "sonner";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
Expand All @@ -12,7 +12,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import {
acceptAccountRequest,
AccountRequest,
declineAccountRequest,
deleteAccountRequest,
getAccountRequests,
} from "@/lib/accountRequests";

Expand All @@ -38,13 +38,31 @@ export function PendingAccountRequests() {
queryFn: getAccountRequests,
});

const requests = data?.success ? data.requests : [];
const requests = useMemo(
() =>
data?.success
? [...data.requests].sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)
: [],
[data],
);

if (isError) {
toast.error("Error", {
description: `Failed to fetch account requests: ${JSON.stringify(error)}`,
});
}
useEffect(() => {
if (isError) {
toast.error("Error", {
description: `Failed to fetch account requests: ${JSON.stringify(error)}`,
});
return;
}

if (data && !data.success && data.error) {
toast.error("Failed to Fetch Account Requests", {
description: data.error,
});
}
}, [data, error, isError]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error toast will re-fire on every query invalidation

The useEffect depends on [data, error, isError]. When a user successfully accepts or declines a request, queryClient.invalidateQueries is called, which triggers a refetch of getAccountRequests. If the refetch returns the same { success: false, error: "..." } result, data will be a new object reference, causing this effect to run again and display a duplicate toast.

A more reliable pattern would be to use the onError / onSettled callbacks directly in the useQuery options, or guard with a useRef to track whether the toast has already been shown for a given error:

const hasShownErrorToast = useRef(false);

useEffect(() => {
  if (isError && !hasShownErrorToast.current) {
    hasShownErrorToast.current = true;
    toast.error("Error", {
      description: `Failed to fetch account requests: ${JSON.stringify(error)}`,
    });
    return;
  }
  if (data && !data.success && data.error && !hasShownErrorToast.current) {
    hasShownErrorToast.current = true;
    toast.error("Failed to Fetch Account Requests", { description: data.error });
  }
}, [data, error, isError]);


const setProcessing = (id: string, processing: boolean) => {
setProcessingIds((prev) => {
Expand All @@ -70,6 +88,7 @@ export function PendingAccountRequests() {
});
queryClient.invalidateQueries({ queryKey: ["accountRequests"] });
queryClient.invalidateQueries({ queryKey: ["users"] });
queryClient.invalidateQueries({ queryKey: ["projects"] });
} else {
toast.error("Failed to Accept Request", {
description: result.error ?? "An unexpected error occurred.",
Expand All @@ -82,9 +101,9 @@ export function PendingAccountRequests() {
},
});

const declineRequestHandler = useMutation({
const deleteRequestHandler = useMutation({
mutationFn: async (request: AccountRequest) =>
declineAccountRequest(request.id),
deleteAccountRequest(request.id),
onMutate: (request) => setProcessing(request.id, true),
onSuccess: (result, request) => {
setProcessing(request.id, false);
Expand Down Expand Up @@ -136,6 +155,11 @@ export function PendingAccountRequests() {
</div>
))}
</div>
) : data && !data.success ? (
<div className="flex flex-col items-center justify-center py-12 text-center text-sm text-muted-foreground gap-2">
<XCircle className="h-8 w-8 text-muted-foreground/40" />
<span>{data.error ?? "Failed to load account requests."}</span>
</div>
) : requests.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center text-sm text-muted-foreground gap-2">
<CheckCircle className="h-8 w-8 text-muted-foreground/40" />
Expand Down Expand Up @@ -185,7 +209,7 @@ export function PendingAccountRequests() {
size="sm"
variant="outline"
disabled={isProcessing}
onClick={() => declineRequestHandler.mutate(request)}
onClick={() => deleteRequestHandler.mutate(request)}
className="gap-1.5 border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground"
>
<XCircle className="h-3.5 w-3.5" />
Expand Down
Loading