-
Notifications
You must be signed in to change notification settings - Fork 264
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'dev' into fix/post-contact-form
- Loading branch information
Showing
5 changed files
with
205 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.", | ||
}; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const apiUrl = process.env.API_URL; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -20,6 +21,7 @@ import { | |
SelectTrigger, | ||
SelectValue, | ||
} from "~/components/ui/select"; | ||
import { useToast } from "~/components/ui/use-toast"; | ||
|
||
interface ModalProperties { | ||
show: boolean; | ||
|
@@ -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); | ||
|
@@ -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(); | ||
} | ||
}; | ||
|
||
|
@@ -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"> | ||
</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"> | ||
</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> | ||
); | ||
}; | ||
|
||
|