diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx
index a325ec9..81f2126 100644
--- a/app/(app)/page.tsx
+++ b/app/(app)/page.tsx
@@ -1,28 +1,69 @@
/**
* @file app/(app)/page.tsx
- * @description Main application layout page featuring a dynamic header and default welcome screen.
+ * @description Main application layout page featuring the FriendsView.
*/
+import { auth } from "@/auth";
+import { db } from "@/db";
+import { friendships } from "@/db/schema";
+import { or, eq } from "drizzle-orm";
import { AppHeader } from "@/components/layout/AppHeader";
+import { FriendsView, Friendship } from "@/components/friends/FriendsView";
+
+export default async function AppPage() {
+ const session = await auth();
+
+ let initialFriendships: Friendship[] = [];
+
+ if (session?.user?.id) {
+ const currentUserId = session.user.id;
+
+ const rawFriendships = await db.query.friendships.findMany({
+ where: or(
+ eq(friendships.senderId, currentUserId),
+ eq(friendships.receiverId, currentUserId),
+ ),
+ with: {
+ sender: {
+ columns: {
+ id: true,
+ username: true,
+ color: true,
+ status: true,
+ },
+ },
+ receiver: {
+ columns: {
+ id: true,
+ username: true,
+ color: true,
+ status: true,
+ },
+ },
+ },
+ });
+
+ initialFriendships = rawFriendships as Friendship[];
+ }
-/**
- * Renders the default application page with the header and central welcome message.
- *
- * @returns {JSX.Element} The rendered application page view.
- */
-export default function AppPage() {
return (
- {/* Dynamic Header */}
-
-
- {/* Main Content */}
-
-
Welcome back!
-
- Select a server from the left side or start a chat with your friends.
-
-
+ {session?.user?.id ? (
+
+ ) : (
+ <>
+
+
+
Welcome back!
+
+ Please sign in to view your friends list or start a conversation.
+
+
+ >
+ )}
);
}
diff --git a/components/friends/AddFriendTab.tsx b/components/friends/AddFriendTab.tsx
new file mode 100644
index 0000000..0d15536
--- /dev/null
+++ b/components/friends/AddFriendTab.tsx
@@ -0,0 +1,98 @@
+/**
+ * @file components/friends/AddFriendTab.tsx
+ * @description Tab component providing a form to send friend requests via username.
+ */
+
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { ActionButton } from "../ui/ActionButton";
+
+/**
+ * Renders a form to add new friends by entering their username.
+ *
+ * @returns {JSX.Element} The rendered add friend tab component.
+ */
+export function AddFriendTab() {
+ const router = useRouter();
+ const [addUsername, setAddUsername] = useState("");
+ const [addError, setAddError] = useState("");
+ const [addSuccess, setAddSuccess] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ /**
+ * Handles the form submission to send a friend request asynchronously.
+ *
+ * @async
+ * @function handleSendRequest
+ * @param {React.FormEvent} e - The form submission event.
+ * @returns {Promise} Resolves when the friend request process completes.
+ */
+ const handleSendRequest = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setAddError("");
+ setAddSuccess("");
+ if (!addUsername.trim()) return;
+
+ setLoading(true);
+ try {
+ const res = await fetch("/api/friends", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ username: addUsername }),
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ setAddError(data.error || "Failed to send request");
+ } else {
+ setAddSuccess(
+ `Success! Your friend request to ${addUsername} was sent.`,
+ );
+ setAddUsername("");
+ router.refresh();
+ }
+ } catch {
+ setAddError("Internal server error");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ Add Friend
+
+
+ You can add friends with their username.
+
+
+
+
+ );
+}
diff --git a/components/friends/AllFriendsTab.tsx b/components/friends/AllFriendsTab.tsx
new file mode 100644
index 0000000..6b3b0da
--- /dev/null
+++ b/components/friends/AllFriendsTab.tsx
@@ -0,0 +1,97 @@
+/**
+ * @file components/friends/AllFriendsTab.tsx
+ * @description Component rendering the list of all accepted friends along with status indicators, messaging options, and remove actions.
+ */
+
+"use client";
+
+import { MessageSquare, X } from "lucide-react";
+import { UserAvatar } from "../ui/UserAvatar";
+import { Friendship, FriendUser } from "./FriendsView";
+
+/**
+ * Properties for the AllFriendsTab component.
+ *
+ * @interface AllFriendsTabProps
+ * @property {Friendship[]} acceptedFriends - Array of accepted friendship objects.
+ * @property {(f: Friendship) => FriendUser} getFriendUser - Helper function to retrieve user details from a friendship relationship.
+ * @property {(id: string) => void} onRemove - Callback function to remove a friend by their friendship ID.
+ */
+interface AllFriendsTabProps {
+ acceptedFriends: Friendship[];
+ getFriendUser: (f: Friendship) => FriendUser;
+ onRemove: (id: string) => void;
+}
+
+/**
+ * Renders the list of all accepted friends along with status, message, and remove actions.
+ *
+ * @param {AllFriendsTabProps} props - The component props.
+ * @param {Friendship[]} props.acceptedFriends - Array of accepted friendship objects.
+ * @param {(f: Friendship) => FriendUser} props.getFriendUser - Helper function to extract user details.
+ * @param {(id: string) => void} props.onRemove - Callback function to remove a friend.
+ * @returns {JSX.Element} The rendered list of all accepted friends.
+ */
+export function AllFriendsTab({
+ acceptedFriends,
+ getFriendUser,
+ onRemove,
+}: AllFriendsTabProps) {
+ return (
+
+
+ All Friends — {acceptedFriends.length}
+
+
+ {acceptedFriends.length === 0 ? (
+
You have no friends yet.
+ ) : (
+
+ {acceptedFriends.map((f) => {
+ const friend = getFriendUser(f);
+ const isOnline = friend.status && friend.status !== "OFFLINE";
+
+ return (
+
+
+
+
+
+
+
+ {friend.username}
+
+
+ {isOnline ? "Online" : "Offline"}
+
+
+
+
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/components/friends/FriendsHeader.tsx b/components/friends/FriendsHeader.tsx
new file mode 100644
index 0000000..246ff8e
--- /dev/null
+++ b/components/friends/FriendsHeader.tsx
@@ -0,0 +1,117 @@
+/**
+ * @file components/friends/FriendsHeader.tsx
+ * @description Renders a responsive sub-header with tabs for navigating between all friends, pending requests, and adding a friend.
+ */
+
+"use client";
+
+import { Users, Clock } from "lucide-react";
+import { ActionButton } from "../ui/ActionButton";
+
+/**
+ * Available tab types for the friends navigation view.
+ *
+ * @type {TabType}
+ */
+export type TabType = "all" | "pending" | "add";
+
+/**
+ * Properties for the FriendsHeader component.
+ *
+ * @interface FriendsHeaderProps
+ * @property {TabType} activeTab - The currently active friends tab identifier.
+ * @property {(tab: TabType) => void} setActiveTab - Callback function to update the active friends tab.
+ * @property {number} [allCount=0] - The total number of friends.
+ * @property {number} [pendingCount=0] - The number of pending friend requests.
+ */
+interface FriendsHeaderProps {
+ activeTab: TabType;
+ setActiveTab: (tab: TabType) => void;
+ allCount?: number;
+ pendingCount?: number;
+}
+
+/**
+ * Renders a responsive sub-header with tabs for navigating between all friends, pending requests, and adding a friend.
+ *
+ * @param {FriendsHeaderProps} props - The component props.
+ * @param {TabType} props.activeTab - The currently active tab.
+ * @param {(tab: TabType) => void} props.setActiveTab - Function to change the active tab.
+ * @param {number} [props.allCount=0] - Total count of all friends.
+ * @param {number} [props.pendingCount=0] - Count of pending friend requests.
+ * @returns {JSX.Element} The rendered friends navigation header component.
+ */
+export function FriendsHeader({
+ activeTab,
+ setActiveTab,
+ allCount = 0,
+ pendingCount = 0,
+}: FriendsHeaderProps) {
+ return (
+
+ );
+}
diff --git a/components/friends/FriendsView.tsx b/components/friends/FriendsView.tsx
new file mode 100644
index 0000000..a70f1c4
--- /dev/null
+++ b/components/friends/FriendsView.tsx
@@ -0,0 +1,194 @@
+/**
+ * @file components/friends/FriendsView.tsx
+ * @description Interactive Discord-style friends list view component orchestrating tabs.
+ */
+
+"use client";
+
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { AppHeader } from "@/components/layout/AppHeader";
+import { TabType } from "@/components/friends/FriendsHeader";
+import { AddFriendTab } from "./AddFriendTab";
+import { PendingRequestsTab } from "./PendingRequestsTab";
+import { AllFriendsTab } from "./AllFriendsTab";
+import { UserStatus } from "@/db/schema";
+
+/**
+ * Status types for a friendship relation.
+ *
+ * @type {FriendshipStatus}
+ */
+export type FriendshipStatus = "PENDING" | "ACCEPTED" | "BLOCKED";
+
+/**
+ * Represents a user within a friendship context.
+ *
+ * @interface FriendUser
+ * @property {string} id - The unique identifier of the user.
+ * @property {string} username - The display username of the user.
+ * @property {string} color - The associated profile color of the user.
+ * @property {UserStatus} status - The current online/activity status of the user.
+ */
+export interface FriendUser {
+ id: string;
+ username: string;
+ color: string;
+ status: UserStatus;
+}
+
+/**
+ * Represents a friendship entry between two users.
+ *
+ * @interface Friendship
+ * @property {string} id - The unique identifier of the friendship record.
+ * @property {string} senderId - The user ID of the friend request sender.
+ * @property {string} receiverId - The user ID of the friend request receiver.
+ * @property {FriendshipStatus} status - The current status of the friendship.
+ * @property {FriendUser} sender - The profile details of the sender user.
+ * @property {FriendUser} receiver - The profile details of the receiver user.
+ */
+export interface Friendship {
+ id: string;
+ senderId: string;
+ receiverId: string;
+ status: FriendshipStatus;
+ sender: FriendUser;
+ receiver: FriendUser;
+}
+
+/**
+ * Properties for the FriendsView component.
+ *
+ * @interface FriendsViewProps
+ * @property {string} currentUserId - The unique identifier of the currently logged-in user.
+ * @property {Friendship[]} initialFriendships - Initial list of friendships fetched from the server.
+ */
+interface FriendsViewProps {
+ currentUserId: string;
+ initialFriendships: Friendship[];
+}
+
+/**
+ * Renders the friends view container, managing navigation tabs, state synchronization, and friendship mutation actions.
+ *
+ * @param {FriendsViewProps} props - The component props.
+ * @param {string} props.currentUserId - The unique identifier of the currently logged-in user.
+ * @param {Friendship[]} props.initialFriendships - Initial list of friendships.
+ * @returns {JSX.Element} The rendered friends view interface.
+ */
+export function FriendsView({
+ currentUserId,
+ initialFriendships,
+}: FriendsViewProps) {
+ const router = useRouter();
+ const [activeTab, setActiveTab] = useState("all");
+ const [friendships, setFriendships] =
+ useState(initialFriendships);
+
+ useEffect(() => {
+ setFriendships(initialFriendships);
+ }, [initialFriendships]);
+
+ /**
+ * Retrieves the counter-party user object from a given friendship record relative to the current user.
+ *
+ * @function getFriendUser
+ * @param {Friendship} f - The friendship object to evaluate.
+ * @returns {FriendUser} The other user involved in the friendship.
+ */
+ const getFriendUser = (f: Friendship): FriendUser => {
+ return f.senderId === currentUserId ? f.receiver : f.sender;
+ };
+
+ const pendingRequests = friendships.filter((f) => f.status === "PENDING");
+ const acceptedFriends = friendships.filter((f) => f.status === "ACCEPTED");
+
+ /**
+ * Handles accepting a pending friend request via API.
+ *
+ * @async
+ * @function handleAccept
+ * @param {string} friendshipId - The unique identifier of the friendship request to accept.
+ * @returns {Promise} Resolves when the request is processed.
+ */
+ const handleAccept = async (friendshipId: string) => {
+ try {
+ const res = await fetch(`/api/friends/${friendshipId}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ status: "ACCEPTED" }),
+ });
+ if (res.ok) {
+ setFriendships((prev) =>
+ prev.map((item) =>
+ item.id === friendshipId ? { ...item, status: "ACCEPTED" } : item,
+ ),
+ );
+ router.refresh();
+ }
+ } catch (err) {
+ console.error("Failed to accept request:", err);
+ }
+ };
+
+ /**
+ * Handles deleting or declining a friendship or pending request via API.
+ *
+ * @async
+ * @function handleDeleteOrDecline
+ * @param {string} friendshipId - The unique identifier of the friendship to delete.
+ * @returns {Promise} Resolves when the deletion is processed.
+ */
+ const handleDeleteOrDecline = async (friendshipId: string) => {
+ try {
+ const res = await fetch(`/api/friends/${friendshipId}`, {
+ method: "DELETE",
+ });
+ if (res.ok) {
+ setFriendships((prev) =>
+ prev.filter((item) => item.id !== friendshipId),
+ );
+ router.refresh();
+ }
+ } catch (err) {
+ console.error("Failed to delete friendship/request:", err);
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ {/* Main Tab Content */}
+
+ {activeTab === "add" &&
}
+
+ {activeTab === "pending" && (
+
+ )}
+
+ {activeTab === "all" && (
+
+ )}
+
+
+ );
+}
diff --git a/components/friends/PendingRequestsTab.tsx b/components/friends/PendingRequestsTab.tsx
new file mode 100644
index 0000000..5fbd171
--- /dev/null
+++ b/components/friends/PendingRequestsTab.tsx
@@ -0,0 +1,107 @@
+/**
+ * @file components/friends/PendingRequestsTab.tsx
+ * @description Tab component that renders a list of pending friend requests with options to accept, decline, or cancel them.
+ */
+
+"use client";
+
+import { Check, X } from "lucide-react";
+import { UserAvatar } from "../ui/UserAvatar";
+import { Friendship, FriendUser } from "./FriendsView";
+
+/**
+ * Properties for the PendingRequestsTab component.
+ *
+ * @interface PendingRequestsTabProps
+ * @property {string} currentUserId - The unique identifier of the currently logged-in user.
+ * @property {Friendship[]} pendingRequests - Array of pending friendship objects.
+ * @property {(f: Friendship) => FriendUser} getFriendUser - Helper function to extract the friend user from a friendship record.
+ * @property {(id: string) => void} onAccept - Callback function triggered to accept an incoming friend request.
+ * @property {(id: string) => void} onDeclineOrCancel - Callback function triggered to decline an incoming request or cancel an outgoing one.
+ */
+interface PendingRequestsTabProps {
+ currentUserId: string;
+ pendingRequests: Friendship[];
+ getFriendUser: (f: Friendship) => FriendUser;
+ onAccept: (id: string) => void;
+ onDeclineOrCancel: (id: string) => void;
+}
+
+/**
+ * Renders a list of pending friend requests with options to accept, decline, or cancel them.
+ *
+ * @param {PendingRequestsTabProps} props - The component props.
+ * @param {string} props.currentUserId - The unique identifier of the currently logged-in user.
+ * @param {Friendship[]} props.pendingRequests - Array of pending friendship objects.
+ * @param {(f: Friendship) => FriendUser} props.getFriendUser - Helper function to extract the friend user.
+ * @param {(id: string) => void} props.onAccept - Callback to accept a request.
+ * @param {(id: string) => void} props.onDeclineOrCancel - Callback to decline or cancel a request.
+ * @returns {JSX.Element} The rendered pending requests tab.
+ */
+export function PendingRequestsTab({
+ currentUserId,
+ pendingRequests,
+ getFriendUser,
+ onAccept,
+ onDeclineOrCancel,
+}: PendingRequestsTabProps) {
+ return (
+
+
+ Pending Requests — {pendingRequests.length}
+
+ {pendingRequests.length === 0 ? (
+
No pending friend requests.
+ ) : (
+
+ {pendingRequests.map((req) => {
+ const isIncoming = req.receiverId === currentUserId;
+ const friend = getFriendUser(req);
+
+ return (
+
+
+
+
+
+
+
+ {friend.username}
+
+
+ {isIncoming ? "Incoming Request" : "Outgoing Request"}
+
+
+
+
+
+ {isIncoming && (
+
+ )}
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/components/layout/AppHeader.tsx b/components/layout/AppHeader.tsx
index 045e039..0f3d999 100644
--- a/components/layout/AppHeader.tsx
+++ b/components/layout/AppHeader.tsx
@@ -1,6 +1,6 @@
/**
* @file components/layout/AppHeader.tsx
- * @description Unified application header supporting general views, chat channels, and DM views.
+ * @description Application top header component providing navigation controls, dynamic channel or friend tab titles, and optional server settings or member list toggles.
*/
"use client";
@@ -8,14 +8,20 @@
import { useSidebarStore } from "@/lib/stores/useSidebarStore";
import { PanelLeftOpen, PanelLeftClose, Users, Hash } from "lucide-react";
import { ServerSettingsMenu } from "./ServerSettingsMenu";
+import { FriendsHeader, TabType } from "@/components/friends/FriendsHeader";
/**
* Properties for the AppHeader component.
*
* @interface AppHeaderProps
- * @property {string} [title] - Optional channel or page title to display in the header.
- * @property {boolean} [showMembersButton=false] - Flag indicating whether to display the member list toggle button.
- * @property {{ id: string; name: string }} [server] - Optional server details to enable server settings & delete functionality.
+ * @property {string} [title] - The title of the current channel or view.
+ * @property {boolean} [showMembersButton=false] - Whether to show the button that toggles the members sidebar.
+ * @property {{ id: string; name: string }} [server] - Optional server configuration object containing its unique identifier and name.
+ * @property {boolean} [showFriendsTabs=false] - Whether to display the friends navigation tabs in the header.
+ * @property {TabType} [activeTab] - The currently active friends tab identifier.
+ * @property {(tab: TabType) => void} [setActiveTab] - Callback function to update the active friends tab.
+ * @property {number} [allCount=0] - The total number of friends.
+ * @property {number} [pendingCount=0] - The number of pending friend requests.
*/
interface AppHeaderProps {
title?: string;
@@ -24,39 +30,55 @@ interface AppHeaderProps {
id: string;
name: string;
};
+ showFriendsTabs?: boolean;
+ activeTab?: TabType;
+ setActiveTab?: (tab: TabType) => void;
+ allCount?: number;
+ pendingCount?: number;
}
/**
- * Renders the application header bar with navigation controls, dynamic page titles, server settings, and member list toggle capability.
+ * Renders the application header with navigation controls, dynamic titles, tabs, and action buttons.
*
* @param {AppHeaderProps} props - The component props.
- * @param {string} [props.title] - Optional channel or page title to display.
- * @param {boolean} [props.showMembersButton=false] - Whether to show the button toggling the right sidebar/member panel.
- * @param {{ id: string; name: string }} [props.server] - Optional server object containing id and name for settings menu.
- * @returns {JSX.Element} The header component visual structure.
+ * @param {string} [props.title] - The title of the current channel or view.
+ * @param {boolean} [props.showMembersButton=false] - Whether to show the members list toggle button.
+ * @param {{ id: string; name: string }} [props.server] - Optional server details object.
+ * @param {boolean} [props.showFriendsTabs=false] - Whether to display the friends tabs.
+ * @param {TabType} [props.activeTab] - The active friends tab.
+ * @param {(tab: TabType) => void} [props.setActiveTab] - Function to change the active friends tab.
+ * @param {number} [props.allCount=0] - Count of all friends.
+ * @param {number} [props.pendingCount=0] - Count of pending friend requests.
+ * @returns {JSX.Element} The rendered application header container.
*/
export function AppHeader({
title,
showMembersButton = false,
server,
+ showFriendsTabs = false,
+ activeTab,
+ setActiveTab,
+ allCount = 0,
+ pendingCount = 0,
}: AppHeaderProps) {
const { isNavOpen, toggleNav, toggleMembers } = useSidebarStore();
- const hasContent = !isNavOpen || !!title || showMembersButton || !!server;
+ const hasContent =
+ !isNavOpen || !!title || showMembersButton || !!server || showFriendsTabs;
return (
-
+
{/* Toggle button for navigation */}
- {/* Dynamic Title (Channel/Page Name) */}
+ {/* Dynamic Channel Title */}
{title && (
-
-
+
+
{title}
)}
+
+ {/* Friends Header Component */}
+ {showFriendsTabs && setActiveTab && activeTab && (
+
+ )}
{/* Right Action Buttons */}
-
- {/* Server Settings Menu (contains the Delete Server action) */}
+
{server && (
)}
- {/* Button for the member bar */}
{showMembersButton && (