Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 19 additions & 14 deletions app/api/project/invite-member/route.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { withAuth } from '@/lib/protectedRoute';
import { generateInvitation } from '@/server/services/inviteProjectMember';
import { withAuth } from "@/lib/protectedRoute";
import { generateInvitation } from "@/server/services/inviteProjectMember";

import { NextResponse } from 'next/server';
import { NextResponse } from "next/server";

export const POST = withAuth(async (request,context ,session) => {
try{
export const POST = withAuth(async (request, context, session) => {
try {
const body = await request.json();
await generateInvitation(body.hackathon_id, body.user_id, session.user.name,body.emails);
return NextResponse.json({ message: 'invitation sent' }, { status: 201 });
}
catch (error: any) {
console.error('Error inviting members:', error);
console.error('Error POST /api/submit-project:', error.message);
const result = await generateInvitation(
body.hackathon_id,
body.user_id,
session.user.name,
body.emails
);
return NextResponse.json(
{ message: "invitation sent", result },
{ status: 200 }
);
} catch (error: any) {
console.error("Error inviting members:", error);
console.error("Error POST /api/submit-project:", error.message);
const wrappedError = error as Error;
return NextResponse.json(
{ error: wrappedError },
{ status: wrappedError.cause == 'ValidationError' ? 400 : 500 }
{ status: wrappedError.cause == "ValidationError" ? 400 : 500 }
);
}

});

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useToast } from '@/hooks/use-toast';

export const InvitationLinksMember = ({invitationResult}:{invitationResult:any}) => {
const { toast } = useToast();
return (
<ul className="space-y-2">
{invitationResult?.InviteLinks?.map((link: any) => (
<li key={link.User}>
<button
onClick={async () => {
try {
await navigator.clipboard.writeText(
link.Invitation
);

toast({
title: "Copied!",
description:
"Invitation link copied to clipboard",
});
} catch (err) {
console.error("Failed to copy link:", err);
toast({
title: "Copy failed",
description: "Could not copy link to clipboard",
variant: "destructive",
});
}
}}
className="flex items-center justify-between w-full p-3 text-left bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg transition-colors cursor-pointer border border-gray-200 dark:border-gray-700"
title="Click to copy invitation link"
>
<span className="font-medium text-gray-900 dark:text-gray-100">
{link.User}
</span>
<span className="text-sm text-gray-500 dark:text-gray-400">
Click to copy link
</span>
</button>
</li>
))}
</ul>

)
}
97 changes: 84 additions & 13 deletions components/hackathons/project-submission/components/Members.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import { LoadingButton } from "@/components/ui/loading-button";
import { ProjectMemberWarningDialog } from "./ProjectMemberWarningDialog";
import { useRouter, useSearchParams } from "next/navigation";
import { MemberStatus } from "@/types/project";
import { useToast } from "@/hooks/use-toast";
import { Toaster } from "@/components/ui/toaster";
import { InvitationLinksMember } from "./InvitationLinksMember";
export default function MembersComponent({
project_id,
hackaton_id,
Expand All @@ -55,6 +58,8 @@ export default function MembersComponent({
const [sendingInvitation, setSendingInvitation] = useState(false);
const [invalidEmails, setInvalidEmails] = useState<string[]>([]);
const [isValidingEmail, setIsValidingEmail] = useState(false);
const [emailSent, setEmailSent] = useState(false);
const [invitationResult, setInvitationResult] = useState<any>(null);
const roles: string[] = [
"Member",
"Developer",
Expand All @@ -65,6 +70,7 @@ export default function MembersComponent({

const router = useRouter();
const searchParams = useSearchParams();
const { toast } = useToast();

const handleAddEmail = () => {
if (newEmail && !emails.includes(newEmail) && validateEmail(newEmail)) {
Expand Down Expand Up @@ -92,12 +98,16 @@ export default function MembersComponent({
await onHandleSave();
}

await axios.post(`/api/project/invite-member`, {
const invitationResult = await axios.post(`/api/project/invite-member`, {
emails: emails,
hackathon_id: hackaton_id,
project_id: project_id,
user_id: user_id,
});
setInvitationResult(invitationResult.data?.result);
if (invitationResult.data?.result?.Success) {
setEmailSent(true);
}
if ((!project_id || project_id === "") && onProjectCreated) {
onProjectCreated();
}
Expand Down Expand Up @@ -169,23 +179,25 @@ export default function MembersComponent({
};

const handleAcceptJoinTeamWithPreviousProject = async (accepted: boolean) => {
if(setOpenCurrentProject){
if (setOpenCurrentProject) {
setOpenCurrentProject(accepted);
}
if (!accepted) {
const params = new URLSearchParams(searchParams.toString());
params.delete("invitation");
await updateMemberStatus(MemberStatus.REJECTED,false);
await updateMemberStatus(MemberStatus.REJECTED, false);
router.push(`/hackathons/project-submission?${params.toString()}`);

return;
}
await updateMemberStatus(MemberStatus.CONFIRMED,true);
handleAcceptJoinTeam(true);

await updateMemberStatus(MemberStatus.CONFIRMED, true);
handleAcceptJoinTeam(true);
};

const updateMemberStatus = async (status: string,wasInOtherProject: boolean) => {
const updateMemberStatus = async (
status: string,
wasInOtherProject: boolean
) => {
try {
axios
.patch(`/api/project/${project_id}/members/status`, {
Expand All @@ -199,11 +211,10 @@ export default function MembersComponent({
.catch((error) => {
console.error("Error updating status:", error);
});

} catch (error) {
console.error("Error joining team:", error);
}
}
};

useEffect(() => {
if (!project_id) return;
Expand Down Expand Up @@ -250,7 +261,9 @@ export default function MembersComponent({
Invite Member
</DialogTitle>
<DialogDescription className="text-sm text-zinc-400 mt-0 pt-0">
Enter the email addresses of the persons you want to invite to your team and then press <strong>Enter</strong>. When you've added all emails, click on <strong>Send Invitation</strong>.
Enter the email addresses of the persons you want to invite to
your team and then press <strong>Enter</strong>. When you've
added all emails, click on <strong>Send Invitation</strong>.
</DialogDescription>
</DialogHeader>
<Card className="border border-red-500 dark:bg-zinc-800 rounded-md">
Expand Down Expand Up @@ -321,7 +334,7 @@ export default function MembersComponent({
</div>
</Card>
</DialogContent>
) : (
) : emailSent ? (
<DialogContent
className="dark:bg-zinc-900
dark:text-white rounded-lg p-6 w-full
Expand Down Expand Up @@ -350,8 +363,63 @@ export default function MembersComponent({
<span className="font-semibold gap-2 text-md">
{emails.join("; ")}
</span>
. They will receive an email to join your team.
. They will receive an email to join your team. You can also
copy the links and send them manually.
</p>
{invitationResult &&
invitationResult?.InviteLinks &&
invitationResult?.InviteLinks.length > 0 && (
<InvitationLinksMember
invitationResult={invitationResult}
/>
)}
<div className="items-center justify-center text-center">
<DialogClose asChild>
<Button
onClick={() => {
setOpenModal(false);
setEmails([]);
setNewEmail("");
setInvitationSent(false);
}}
className="dark:bg-white border rounder-md max-w-16 "
>
Done
</Button>
</DialogClose>
</div>
</div>
</Card>
</DialogContent>
) : (
<DialogContent
className="dark:bg-zinc-900
dark:text-white rounded-lg p-6 w-full
max-w-md border border-zinc-400 px-4"
hideCloseButton={true}
>
<DialogClose asChild>
<Button
variant="ghost"
size="icon"
className="absolute top-6 right-4 dark:text-white hover:text-red-400 p-0 h-6 w-6"
>
</Button>
</DialogClose>
<DialogTitle className="text-lg font-semibold">
Invitation Failed!
</DialogTitle>
<Card className="border border-red-500 dark:bg-zinc-800 rounded-md mt-4">
<div className="flex flex-col px-4 py-2 gap-4">
<div className="flex items-center justify-center text-center">
<BadgeCheck width={35} height={35} color="#FF394A" />
</div>
<p className=" text-md ">
We've got some errors sending the invitations, but here are
the links for you to send them manually:{" "}
</p>
<InvitationLinksMember invitationResult={invitationResult} />
<div className="items-center justify-center text-center">
<DialogClose asChild>
<Button
Expand Down Expand Up @@ -471,6 +539,9 @@ export default function MembersComponent({
hackathonId={hackaton_id as string}
setLoadData={handleAcceptJoinTeamWithPreviousProject}
/>

{/* ✅ TOASTER: Required for toast notifications */}
<Toaster />
</>
);
}
Loading