diff --git a/src/app/(private)/friends/_components/AddFriendButton.tsx b/src/app/(private)/friends/_components/AddFriendButton.tsx index 83e3d08..33e43a4 100644 --- a/src/app/(private)/friends/_components/AddFriendButton.tsx +++ b/src/app/(private)/friends/_components/AddFriendButton.tsx @@ -1,14 +1,14 @@ "use client"; -import React from 'react'; -import { UserRoundPlus } from 'lucide-react'; +import React from "react"; +import { UserRoundPlus } from "lucide-react"; export default function AddFriendButton() { - return ( - - ); -} \ No newline at end of file + return ( + + ); +} diff --git a/src/app/(private)/friends/_components/FriendRequest.tsx b/src/app/(private)/friends/_components/FriendRequest.tsx index cd0c0a8..9ca545d 100644 --- a/src/app/(private)/friends/_components/FriendRequest.tsx +++ b/src/app/(private)/friends/_components/FriendRequest.tsx @@ -5,6 +5,7 @@ interface FriendRequestProps { name: string; onAccept: () => void; onDeny: () => void; + onCancel: () => void; isSender: boolean; } @@ -12,6 +13,7 @@ export default function FriendRequest({ name, onAccept, onDeny, + onCancel, isSender, }: FriendRequestProps) { { @@ -29,7 +31,7 @@ export default function FriendRequest({ {isSender ? ( diff --git a/src/app/(private)/friends/_components/Friends.tsx b/src/app/(private)/friends/_components/Friends.tsx index 9043e46..8b938e8 100644 --- a/src/app/(private)/friends/_components/Friends.tsx +++ b/src/app/(private)/friends/_components/Friends.tsx @@ -8,15 +8,16 @@ import { getApiBase } from "@/utils/etc/apiBase"; import { useUser } from "@/utils/context/userContext"; import { toast } from "react-hot-toast"; import Loading from "@/app/components/loading"; +import AddFriendModal from "../../hangouts/_components/AddFriendModal"; type FetchFriendsResponse = { status: string; friends: Friend[]; }; -type Friend = { +export type Friend = { id: string; - friend_uuid: string; + friend_auth_id: string; friend_email: string; friend_username: string; status: string; @@ -37,10 +38,15 @@ export default function Friends() { const [query, setQuery] = useState(""); const [suggestion, setSuggestion] = useState(null); const [loading, setLoading] = useState(false); + const [hasTrigged, setHasTrigged] = useState(false); const user = useUser(); + const [isModalOpen, setIsModalOpen] = useState(false); async function fetchFriends() { - setLoading(true); + if (!hasTrigged) { + setLoading(true); + setHasTrigged(true); + } const base = getApiBase(); const response = await fetch(`${base}/fetch-friends`, { method: "POST", @@ -91,7 +97,11 @@ export default function Friends() { } } - async function declineFriendRequest(friendshipId: string, removing: boolean) { + async function declineFriendRequest( + friendshipId: string, + removing: boolean, + canceling?: boolean + ) { const base = getApiBase(); const response = await fetch(`${base}/remove-friend`, { method: "POST", @@ -104,12 +114,17 @@ export default function Friends() { }); if (response.ok) { - const toastSuccessMessage = removing + let toastSuccessMessage = removing ? "Succesfully removed friend" : "Succesfully declined friend request"; - const toastErrorMessage = removing + let toastErrorMessage = removing ? "Unable to removed friend" : "Unable to decline friend request"; + if (canceling) { + toastSuccessMessage = "Succesfully canceled pending friend request."; + toastErrorMessage = "Unable to cancel pending friend-request"; + } + const data = (await response.json()) as APIResponse; if (data.status !== 200) { toast.error(toastErrorMessage, { @@ -145,37 +160,93 @@ export default function Friends() { }, [query]); return ( -
-
Friends
- {loading ? ( - - ) : ( - <> -
- -
- {suggestion ? ( - suggestion?.map((friend) => { - if (friend.status === "pending") { - return ( - { - acceptFriendRequest(friend.id); - }} - onDeny={() => { - declineFriendRequest(friend.id, false); - }} - /> - ); - } else - return ( + <> + friend.friend_auth_id)} + pendingFriends={pendingFriends.map((friend) => friend.friend_auth_id)} + isOpen={isModalOpen} + onClose={(rerender: boolean) => { + if (rerender) { + fetchFriends(); + } + setIsModalOpen(false); + }} + /> + +
+
Friends
+ {loading ? ( + + ) : ( + <> +
+ +
{ + setIsModalOpen(true); + }} + > + +
+
+ {suggestion ? ( + suggestion?.map((friend) => { + if (friend.status === "pending") { + return ( +
+ { + acceptFriendRequest(friend.id); + }} + onDeny={() => { + declineFriendRequest(friend.id, false); + }} + onCancel={() => { + declineFriendRequest(friend.id, false, true); + }} + /> +
+ ); + } else + return ( +
+ { + declineFriendRequest(friend.id, true); + }} + /> +
+ ); + }) + ) : ( +
+ {pendingFriends.map((friend) => (
+ { + acceptFriendRequest(friend.id); + }} + onDeny={() => { + declineFriendRequest(friend.id, false); + }} + onCancel={() => { + declineFriendRequest(friend.id, false, true); + }} + /> +
+ ))} + {friends.map((friend) => ( +
{ @@ -183,42 +254,12 @@ export default function Friends() { }} />
- ); - }) - ) : ( -
- {pendingFriends.map((friend) => ( -
- { - acceptFriendRequest(friend.id); - }} - onDeny={() => { - declineFriendRequest(friend.id, false); - }} - /> -
- ))} - {friends.map((friend) => ( -
- { - declineFriendRequest(friend.id, true); - }} - /> -
- ))} -
- )} - -
- -
- - )} -
+ ))} +
+ )} + + )} +
+ ); } diff --git a/src/app/(private)/friends/actions.ts b/src/app/(private)/friends/actions.ts new file mode 100644 index 0000000..42420ac --- /dev/null +++ b/src/app/(private)/friends/actions.ts @@ -0,0 +1,27 @@ +"use server"; +import { createClient } from "@/utils/supabase/server"; + +export type Suggestions = { + auth_id: string; + username: string; +}[]; + +export async function findPeople(username: string) { + const supabase = await createClient(); + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user?.id) { + console.error("User not authenticated"); + return { status: 401, error: "User not authenticated" }; + } + + const { data, error } = await supabase + .from("users") + .select("auth_id, username") + .ilike("username", `%${username}%`); + + return data as Suggestions; +} diff --git a/src/app/(private)/hangouts/_components/AddFriendModal.tsx b/src/app/(private)/hangouts/_components/AddFriendModal.tsx new file mode 100644 index 0000000..18f8444 --- /dev/null +++ b/src/app/(private)/hangouts/_components/AddFriendModal.tsx @@ -0,0 +1,140 @@ +"use client"; + +import React, { ChangeEvent, useRef, useState } from "react"; +import { CheckIcon, XIcon } from "@/app/components/Icons"; +import { getApiBase } from "@/utils/etc/apiBase"; +import toast from "react-hot-toast"; +import { useUser } from "@/utils/context/userContext"; +import SearchBar from "../../friends/_components/SearchBar"; +import { findPeople, Suggestions } from "../../friends/actions"; + +type AddFriendModalProps = { + isOpen: boolean; + friends: string[]; + pendingFriends: string[]; + onClose(rerender: boolean): void; // closes modal +}; + +const TIMER: number = 150; // ms + +const AddFriendModal = ({ + isOpen, + friends, + onClose, + pendingFriends, +}: AddFriendModalProps) => { + if (!isOpen) return null; + const [people, setPeople] = useState([]); + const [requestSent, setRequestSent] = useState([]); + const timeoutRef = useRef(null); + const user = useUser(); + + const handleChange = (e: ChangeEvent) => { + // debounce + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(async () => { + const username = e.target.value; + if (username.trim() === "" || !username) { + setPeople([]); + return; + } + const suggestions = await findPeople(e.target.value); + + if (Array.isArray(suggestions)) { + setPeople( + suggestions.filter((person) => { + if (!user) { + return; + } + return ( + !friends.includes(user.auth_id) && person.auth_id != user?.auth_id + ); + }) + ); + } + }, TIMER); + }; + + const sendFriendRequest = async (idToAdd: string) => { + const base = getApiBase(); + const response = await fetch(`${base}/send-friend-request`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user_A: user?.auth_id, + user_B: idToAdd, + }), + }); + if (!response.ok) { + console.error("Failed to add friend", response.statusText); + toast.error("Error while sending friend request."); + } else { + toast.success("Successfully sent friend request."); + setRequestSent((prev) => [...prev, idToAdd]); + } + }; + + return ( + <> + {isOpen && ( +
+
+
{ + onClose(requestSent.length > 0); + }} + > + +
+
+

+ Search for a friend +

+
+
+ +
+
+ {people.map((person) => { + return ( +
+

{person.username}

+ {requestSent.includes(person.auth_id) || + pendingFriends.includes(person.auth_id) ? ( +
+ +
+ ) : ( + + )} +
+ ); + })} +
+
+
+ )} + + ); +}; + +export default AddFriendModal; diff --git a/src/app/components/Icons.tsx b/src/app/components/Icons.tsx index 82d7445..189fd30 100644 --- a/src/app/components/Icons.tsx +++ b/src/app/components/Icons.tsx @@ -17,3 +17,22 @@ export const XIcon = () => { ); }; + +export const CheckIcon = () => { + return ( + + + + ); +};