Skip to content

Commit

Permalink
Merge branch 'dev' into fix/post-contact-form
Browse files Browse the repository at this point in the history
  • Loading branch information
kiisi authored Aug 16, 2024
2 parents 75dc025 + 4c09f9b commit d7dc307
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 81 deletions.
90 changes: 90 additions & 0 deletions src/actions/inviteMembers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use server";

import axios from "axios";
import * as z from "zod";

import { auth } from "~/lib/auth";
import { getApiUrl } from "./getApiUrl";

// Schema for validating email input
const inviteSchema = z.object({
emails: z.string().nonempty("Emails are required"),
org_id: z.string().optional(),
});

export const inviteMembers = async (emails: string, org_id?: string) => {
const apiUrl = await getApiUrl();
const session = await auth();

const validatedFields = inviteSchema.safeParse({ emails, org_id });
if (!validatedFields.success) {
return {
error: "Invite Failed. Please check your inputs.",
};
}

try {
const response = await axios.post(
`${apiUrl}/api/v1/organisations/invites/send`,
{
emails: emails.split(",").map((email) => email.trim()), // Convert emails string to an array
org_id, // Use organization id if provided
},
{
headers: {
Authorization: `Bearer ${session?.access_token}`,
},
},
);

return {
data: response.data.data,
status: response.status,
};
} catch (error) {
return axios.isAxiosError(error) && error.response
? {
error: error.response.data.message || "Failed to send invites.",
status: error.response.status,
}
: {
error: "An unexpected error occurred.",
};
}
};

export const fetchOrganizations = async () => {
const apiUrl = await getApiUrl();
const session = await auth();

try {
const response = await axios.get(`${apiUrl}/api/v1/organisations`, {
headers: {
Authorization: `Bearer ${session?.access_token}`,
},
});

// Extracting the relevant data
const organizations = response.data.data.map(
(org: { id: string; name: string }) => ({
id: org.id,
name: org.name,
}),
);

return {
data: organizations,
status: response.status,
};
} catch (error) {
return axios.isAxiosError(error) && error.response
? {
error:
error.response.data.message || "Failed to fetch organizations.",
status: error.response.status,
}
: {
error: "An unexpected error occurred.",
};
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import CustomButton from "~/components/common/common-button/common-button";
import { Card, CardContent, CardTitle } from "~/components/ui/card";
import { Switch } from "~/components/ui/switch";
import useApiUrl from "../../organization/members/action/member";
import useApiUrl from "../../organization/members/member";
import MemberCard from "../MemberCard";
import DeleteSuccessModal from "../MemberDeleteModal";
import InviteModal from "../MemberInviteModal";
Expand Down
1 change: 1 addition & 0 deletions src/components/common/modals/invite-member/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const apiUrl = process.env.API_URL;
193 changes: 113 additions & 80 deletions src/components/common/modals/invite-member/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import axios from "axios";
import { Link2Icon } from "lucide-react";
import { useState } from "react";
import { useCallback, useState } from "react";

import { fetchOrganizations, inviteMembers } from "~/actions/inviteMembers";
import CustomButton from "~/components/common/common-button/common-button";
import {
Dialog,
Expand All @@ -20,6 +21,7 @@ import {
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { useToast } from "~/components/ui/use-toast";

interface ModalProperties {
show: boolean;
Expand All @@ -29,9 +31,27 @@ interface ModalProperties {
const InviteMemberModal: React.FC<ModalProperties> = ({ show, onClose }) => {
const [emails, setEmails] = useState("");
const [organization, setOrganization] = useState("");
const [organizations, setOrganizations] = useState<
{ id: string; name: string }[]
>([]);
const [inviteLink, setInviteLink] = useState("");
const [linkGenerated, setLinkGenerated] = useState(false);
const [error, setError] = useState("");
const [organizationsLoaded, setOrganizationsLoaded] = useState(false);

const { toast } = useToast();

const loadOrganizations = useCallback(async () => {
if (organizationsLoaded) return;

const response = await fetchOrganizations();
if (response?.error) {
setError(response.error);
} else {
setOrganizations(response.data || []);
setOrganizationsLoaded(true);
}
}, [organizationsLoaded]);

const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setEmails(event.target.value);
Expand All @@ -41,18 +61,26 @@ const InviteMemberModal: React.FC<ModalProperties> = ({ show, onClose }) => {
setOrganization(value);
};

const handleOrganizationDropdownOpen = () => {
if (!organizationsLoaded) {
loadOrganizations();
}
};

const handleSubmit = async () => {
try {
const response = await axios.post("/api/v1/invite/create", {
emails: emails.split(",").map((email) => email.trim()),
organization,
setError("");

const response = await inviteMembers(emails, organization);
if (response?.error) {
setError(response.error);
} else {
toast({
title: "Success",
description: "Your invite has been sent successfully to members email",
variant: "default",
});
if (response.status === 200) {
alert("Invites sent successfully!");
onClose();
}
} catch {
setError("Failed to send invites.");
setEmails("");
onClose();
}
};

Expand All @@ -63,84 +91,89 @@ const InviteMemberModal: React.FC<ModalProperties> = ({ show, onClose }) => {
setInviteLink(response.data.invite_link);
setLinkGenerated(true);
navigator.clipboard.writeText(response.data.invite_link);
alert("Invite link copied to clipboard!");
toast({
title: "Invite Link",
description: "Invite link copied to clipboard!",
});
}
} catch {
setError("Failed to generate invite link.");
}
};

return (
<>
<Dialog open={show} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2 border-b border-gray-300">
<div className="mt-[-8px] flex h-10 w-10 items-center justify-center rounded-full bg-secondary text-center">
KP
</div>
<h2 className="mb-2 text-left text-lg text-neutral-dark-2">
Invite to your Organization
</h2>
</DialogTitle>
<DialogDescription>
<div className="mb-7 mt-6">
<label className="mb-2 block text-left text-base text-neutral-dark-2">
Email
</label>
<input
type="text"
placeholder="[email protected], [email protected]..."
className="w-full rounded-md border border-border px-3 py-2 shadow-sm outline-none focus:border-primary focus:ring-ring"
value={emails}
onChange={handleEmailChange}
/>
</div>
<div>
<label className="mb-2 block text-left text-base text-neutral-dark-2">
Add to Organization (Optional)
</label>
<Select onValueChange={handleOrganizationChange}>
<SelectTrigger className="bg-white">
<SelectValue placeholder="Select Organization" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="org1">Org 1</SelectItem>
<SelectItem value="org2">Org 2</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="mt-8 flex items-center justify-between gap-4">
<span className="relative flex items-center text-primary">
<Link2Icon className="pointer-events-none absolute ml-3" />
<CustomButton
variant="subtle"
onClick={handleInviteWithLink}
className="pl-10"
>
Invite with link
</CustomButton>
</span>

<CustomButton variant="primary" onClick={handleSubmit}>
Send Invites
<Dialog open={show} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2 border-b border-gray-300">
<div className="mt-[-8px] flex h-10 w-10 items-center justify-center rounded-full bg-secondary text-center">
0P
</div>
<h2 className="mb-2 text-left text-lg text-neutral-dark-2">
Invite to your Organization
</h2>
</DialogTitle>
<DialogDescription>
<div className="mb-7 mt-6">
<label className="mb-2 block text-left text-base text-neutral-dark-2">
Email
</label>
<input
type="text"
placeholder="[email protected], [email protected]..."
className="w-full rounded-md border border-border px-3 py-2 shadow-sm outline-none focus:border-primary focus:ring-ring"
value={emails}
onChange={handleEmailChange}
/>
</div>
<div>
<label className="mb-2 block text-left text-base text-neutral-dark-2">
Add to Organization (Optional)
</label>
<Select
onOpenChange={handleOrganizationDropdownOpen}
onValueChange={handleOrganizationChange}
>
<SelectTrigger className="bg-white">
<SelectValue placeholder="Select Organization" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{organizations.map((org) => (
<SelectItem key={org.id} value={org.id}>
{org.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="mt-8 flex items-center justify-between gap-4">
<span className="relative flex items-center text-primary">
<Link2Icon className="pointer-events-none absolute ml-3" />
<CustomButton
variant="subtle"
onClick={handleInviteWithLink}
className="pl-10"
>
Invite with link
</CustomButton>
</span>

<CustomButton variant="primary" onClick={handleSubmit}>
Send Invites
</CustomButton>
</div>
{linkGenerated && (
<div className="mt-4 text-sm text-green-500">
Invite link: {inviteLink}
</div>
{linkGenerated && (
<div className="mt-4 text-sm text-green-500">
Invite link: {inviteLink}
</div>
)}
{error && (
<div className="mt-4 text-sm text-red-500">{error}</div>
)}
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</>
)}
{error && <div className="mt-4 text-sm text-red-500">{error}</div>}
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
);
};

Expand Down

0 comments on commit d7dc307

Please sign in to comment.