diff --git a/apps/web/src/api/index.ts b/apps/web/src/api/index.ts index 36510ba..254845a 100644 --- a/apps/web/src/api/index.ts +++ b/apps/web/src/api/index.ts @@ -6,7 +6,12 @@ import { } from "@repo/typiclient"; import type rootRouter from "@repo/api/router"; -const api = createTypiClient({ +const api = createTypiClient< + typeof rootRouter, + { + credentials: "include"; + } +>({ baseUrl: "http://localhost:5000/api/v1", options: { credentials: "include", @@ -45,7 +50,7 @@ const api = createTypiClient({ !config._retry ) { config._retry = true; - await api.auth["refresh-token"].get(); + await api.auth["refresh-token"].post(); return await retry(); } }, diff --git a/apps/web/src/app/(private)/(chats)/page.tsx b/apps/web/src/app/(private)/(chats)/page.tsx deleted file mode 100644 index f6e61cf..0000000 --- a/apps/web/src/app/(private)/(chats)/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import CurrentChat from "./_components/current-chat"; -import ChatList from "./_components/chat-list"; - -const ChatsPage = async () => { - return ( -
- - -
- ); -}; - -export default ChatsPage; diff --git a/apps/web/src/app/(private)/(main)/_components/call-list.tsx b/apps/web/src/app/(private)/(main)/_components/call-list.tsx new file mode 100644 index 0000000..c19e72b --- /dev/null +++ b/apps/web/src/app/(private)/(main)/_components/call-list.tsx @@ -0,0 +1,199 @@ +"use client"; + +import { + Phone, + Video, + PhoneIncoming, + PhoneMissed, + Clock, + Users, +} from "lucide-react"; +import { useQuery } from "@tanstack/react-query"; +import { formatDate } from "date-fns"; +import { ScrollArea } from "@repo/ui/components/scroll-area"; +import api, { ApiOutputs } from "@repo/web/api"; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@repo/ui/components/avatar"; +import { cn } from "@repo/ui/lib/utils"; +import { Button } from "@repo/ui/components/button"; +import { useSearch } from "../_providers/search-provider"; + +type Call = ApiOutputs["/call"]["/"]["get"]["calls"][number]; + +const CallList = () => { + const { searchQuery } = useSearch(); + const { isPending, isError, data } = useQuery({ + queryKey: ["calls"], + queryFn: async () => { + const { data } = await api.call[""].get({ + options: { + throwOnErrorStatus: true, + }, + }); + return data; + }, + }); + + if (isPending) { + return

Loading...

; + } + + if (isError) { + return

Error

; + } + + const filteredCalls = searchQuery.trim() + ? data.calls.filter((call) => + call.participants.some((p) => + p.user.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) + ) + : data.calls; + + return ( + + {filteredCalls.map((call, index) => { + return ( +
+ + +
+ ); + })} +
+ ); +}; + +const Call = ({ call }: { call: Call }) => { + return ( +
+
+ +
+
+ + +
+
+ ); +}; + +const DateDivider = ({ + call, + prevCall, +}: { + call: Call; + prevCall: Call | undefined; +}) => { + const showDate = + prevCall?.self.joinedAt?.toDateString() !== + call.self.joinedAt!.toDateString(); + + if (!showDate) return null; + + return ( + + {formatDate(call.self.joinedAt!, "PPP")} + + ); +}; + +const CallParticipants = ({ call }: { call: Call }) => { + const displayParticipants = call.participants.slice(0, 3); + const remainingCount = call.participants.length - 3; + + return call.participants && call.participants.length > 1 ? ( +
+
+ {displayParticipants.map((participant) => { + return ( + + + {participant.user.name} + + ); + })} + {remainingCount > 0 && ( +
+ + +{remainingCount} + +
+ )} +
+
+ + + {call.participants.length === 2 + ? call.participants.map((p) => p.user.name).join(", ") + : `${call.participants[0].user.name} and ${call.participants.length - 1} others`} + +
+
+ ) : ( +
+ + + {call.participants[0].user.name} + + {call.participants[0].user.name} +
+ ); +}; + +const CallStatus = ({ call }: { call: Call }) => { + return ( +
+ + + {call.call.status} + + {call.call.duration && ( + <> + +
+ + + {call.call.duration} + +
+ + )} +
+ ); +}; + +const CallIcon = ({ call }: { call: Call }) => { + if (call.call.status === "ringing") + return ; + + if (call.call.status === "missed" || call.call.status === "declined") + return ; + + return ; +}; + +const CallButtons = ({ call }: { call: Call }) => { + return ( +
+ + +
+ ); +}; + +export default CallList; diff --git a/apps/web/src/app/(private)/(chats)/_components/chat-list.tsx b/apps/web/src/app/(private)/(main)/_components/chat-list.tsx similarity index 50% rename from apps/web/src/app/(private)/(chats)/_components/chat-list.tsx rename to apps/web/src/app/(private)/(main)/_components/chat-list.tsx index f35fb29..44084e6 100644 --- a/apps/web/src/app/(private)/(chats)/_components/chat-list.tsx +++ b/apps/web/src/app/(private)/(main)/_components/chat-list.tsx @@ -1,10 +1,7 @@ "use client"; import React from "react"; -import { Plus, Search } from "lucide-react"; -import { Button } from "@repo/ui/components/button"; import { ScrollArea } from "@repo/ui/components/scroll-area"; -import { Input } from "@repo/ui/components/input"; import { Badge } from "@repo/ui/components/badge"; import { Avatar, @@ -12,42 +9,12 @@ import { AvatarImage, } from "@repo/ui/components/avatar"; import api from "@repo/web/api"; -import { SearchProvider, useSearch } from "../_providers/search-context"; +import { useSearch } from "../_providers/search-provider"; import { useQuery } from "@tanstack/react-query"; -import { useCurrentChat } from "@repo/web/app/(private)/_providers/current-chat-provider"; +import { useCurrentChat } from "@repo/web/app/(private)/(main)/_providers/current-chat-provider"; import { cn } from "@repo/ui/lib/utils"; import { format, isSameWeek } from "date-fns"; -const SearchChats = () => { - const { searchQuery, setSearchQuery } = useSearch(); - - return ( -
- - setSearchQuery(e.target.value)} - /> -
- ); -}; - -const Header = () => { - return ( -
-
-

Chats

- -
- -
- ); -}; - const Chat = ({ id, picture, @@ -65,17 +32,17 @@ const Chat = ({ }) => { const { chatId, setChatId } = useCurrentChat(); - const messageFormatDate = (date: Date) => { - return isSameWeek(new Date(), date) - ? format(date, "iiii") - : format(date, "dd/MM/yyyy"); - }; + const messageDate = lastMessageTime + ? isSameWeek(new Date(), lastMessageTime) + ? format(lastMessageTime, "iiii") + : format(lastMessageTime, "dd/MM/yyyy") + : ""; return (

{name}

{lastMessageTime && ( - - {messageFormatDate(lastMessageTime)} - + {messageDate} )}
@@ -109,14 +74,17 @@ const Chat = ({ ); }; -const Chats = () => { +const ChatList = () => { const { searchQuery } = useSearch(); const { isPending, isError, data } = useQuery({ queryKey: ["chats"], queryFn: async () => { - const { status, data } = await api.chat[""].get(); - if (status === "OK") return data; - throw new Error(data.error.message); + const { data } = await api.chat[""].get({ + options: { + throwOnErrorStatus: true, + }, + }); + return data; }, }); @@ -131,35 +99,20 @@ const Chats = () => { : data.chats; return ( - -
-
- {filteredChats.map((chat) => ( - - ))} -
-
+ + {filteredChats.map((chat) => ( + + ))} ); }; -const ChatList = () => { - return ( -
- -
- -
-
- ); -}; - export default ChatList; diff --git a/apps/web/src/app/(private)/(chats)/_components/current-chat.tsx b/apps/web/src/app/(private)/(main)/_components/current-chat.tsx similarity index 67% rename from apps/web/src/app/(private)/(chats)/_components/current-chat.tsx rename to apps/web/src/app/(private)/(main)/_components/current-chat.tsx index 083140d..53ecf16 100644 --- a/apps/web/src/app/(private)/(chats)/_components/current-chat.tsx +++ b/apps/web/src/app/(private)/(main)/_components/current-chat.tsx @@ -30,6 +30,9 @@ import { Crown, ImageIcon, Users, + ArrowLeft, + Search, + Calendar, } from "lucide-react"; import data from "@emoji-mart/data"; import Picker from "@emoji-mart/react"; @@ -67,45 +70,27 @@ import { TabsList, TabsTrigger, } from "@repo/ui/components/tabs"; -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerDescription, - DrawerFooter, - DrawerHeader, - DrawerTitle, - DrawerTrigger, -} from "@repo/ui/components/drawer"; import api, { type ApiOutputs } from "@repo/web/api"; -import { useCurrentChat } from "@repo/web/app/(private)/_providers/current-chat-provider"; +import { useCurrentChat } from "@repo/web/app/(private)/(main)/_providers/current-chat-provider"; import { useInView } from "react-intersection-observer"; +import useMediaQuery from "@repo/web/hooks/use-media-query"; +import { useUserId } from "../_hooks/user-provider"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetClose, +} from "@repo/ui/components/sheet"; +import { + SearchDrawerProvider, + useSearchDrawer, +} from "../_providers/search-drawer-provider copy"; type ChatMessage = - ApiOutputs["/chat"]["/:chatId/messages"]["get"]["data"][number]; + ApiOutputs["/chat"]["/:chatId/messages"]["get"]["messages"][number]; type ChatMessageAttachment = - ApiOutputs["/chat"]["/:chatId/messages"]["get"]["data"][number]["attachments"][number]; - -const CurrentChat = () => { - const { status, fetchStatus, error, isLoading } = useCurrentChatDetails(); - - if (status === "pending" && fetchStatus === "idle") - return ( -
- Maybe select a chat first? -
- ); - - if (isLoading) return
Loading...
; - - return ( -
- - - -
- ); -}; + ApiOutputs["/chat"]["/:chatId/messages"]["get"]["messages"][number]["attachments"][number]; const useCurrentChatDetails = () => { const { chatId } = useCurrentChat(); @@ -113,14 +98,18 @@ const useCurrentChatDetails = () => { queryKey: ["chatDetails", chatId], enabled: chatId !== null, queryFn: async () => { - const { status, data } = await api.chat[":chatId"].get({ - path: { - chatId: chatId!.toString(), + const { data } = await api.chat[":chatId"].get({ + input: { + path: { + chatId: chatId!.toString(), + }, + }, + options: { + throwOnErrorStatus: true, }, }); - if (status === "OK") return data; - throw new Error(data.error.message); + return data; }, }); }; @@ -132,65 +121,123 @@ const useToggleBlockUser = () => { return useMutation({ mutationFn: async () => { - if (!data || !data.otherUser) return; - await api.block[":userId"][data.otherUser.isBlocked ? "delete" : "post"]({ - path: { - userId: data.otherUser.id.toString(), + if (!data || !data.chat.otherUser) return; + await api.block[":userId"][ + data.chat.otherUser.isBlocked ? "delete" : "post" + ]({ + input: { + path: { + userId: data.chat.otherUser.id.toString(), + }, + }, + options: { + throwOnErrorStatus: true, }, }); }, - // When mutate is called: onMutate: async () => { - // Cancel any outgoing refetches - // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ["chatDetails", chatId] }); - - // Snapshot the previous value - const previousIsBlocked = data?.otherUser?.isBlocked; - - // Optimistically update to the new value + const previousIsBlocked = data?.chat?.otherUser?.isBlocked; queryClient.setQueryData(["chatDetails", chatId], () => ({ ...data, - otherUser: { - ...data!.otherUser, - isBlocked: !data!.otherUser!.isBlocked, + chat: { + ...data!.chat, + otherUser: { + ...data!.chat.otherUser, + isBlocked: !data!.chat.otherUser!.isBlocked, + }, }, })); - - // Return a context object with the snapshotted value return { previousIsBlocked }; }, - // If the mutation fails, - // use the context returned from onMutate to roll back - onError: (err, newIsBlocked, context) => { + onSuccess: (_, __, context) => { + toast.success( + `Successfully ${context?.previousIsBlocked ? "unblocked" : "blocked"} user.` + ); + }, + onError: (_, __, context) => { queryClient.setQueryData( ["chatDetails", chatId], context?.previousIsBlocked ); + toast.error( + `Failed to ${context?.previousIsBlocked ? "unblock" : "block"} user.` + ); }, - // Always refetch after error or success: onSettled: () => { queryClient.invalidateQueries({ queryKey: ["chatDetails", chatId] }); }, }); }; -const useUser = () => { - const { data } = useQuery({ - queryKey: ["user"], - queryFn: async () => { - const { status, data } = await api.user[""].get(); - if (status === "OK") return data; - throw new Error(data.error.message); - }, - }); +const useMessageScroll = () => { + const { chatId } = useCurrentChat(); + const queryClient = useQueryClient(); - return { - userId: data?.id ?? null, + const scrollToMessage = async (messageId: number) => { + const messageElement = document.getElementById(`message-${messageId}`); + + if (messageElement) { + messageElement.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + + messageElement.classList.add("highlight-message"); + setTimeout(() => { + messageElement.classList.remove("highlight-message"); + }, 3000); + } else { + // Message not in DOM - need to load more messages + // This could be enhanced to fetch specific pages or use a different API endpoint + // that loads messages around a specific message ID + + try { + // Invalidate and refetch messages to ensure we have the latest data + await queryClient.invalidateQueries({ + queryKey: ["chatMessages", chatId], + }); + + // Wait a bit for the query to complete and DOM to update + setTimeout(() => { + const retryElement = document.getElementById(`message-${messageId}`); + if (retryElement) { + retryElement.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + retryElement.classList.add("highlight-message"); + setTimeout(() => { + retryElement.classList.remove("highlight-message"); + }, 3000); + } + }, 1000); + } catch (error) { + console.error("Failed to load message:", error); + toast.error("Could not navigate to message"); + } + } }; + + return { scrollToMessage }; }; -const ChatHeader = () => { +const CurrentChat = () => { + return ( +
+ +
+ + + + +
+ ); +}; + +const Header = () => { + const { isMobile } = useMediaQuery(); + const { setChatId } = useCurrentChat(); const { data, isPending, isError } = useCurrentChatDetails(); if (isPending) return null; @@ -199,21 +246,29 @@ const ChatHeader = () => { if (!data) return null; return ( -
+
+ {isMobile && ( + + )} - - {data.name.slice(0, 1)} + + {data.chat.name.slice(0, 1)}
-

{data.name}

- {data?.type === "direct" && ( +

{data.chat.name}

+ {data?.chat.type === "direct" && (

- {data.otherUser.status + {data.chat.otherUser.status ? "Online" - : `Last seen ${formatDistanceToNow(data.otherUser.lastSeen, { - addSuffix: true, - })}`} + : `Last seen ${formatDistanceToNow( + data.chat.otherUser.lastSeen, + { + addSuffix: true, + } + )}`}

)}
@@ -236,7 +291,7 @@ const ProfileDialog = () => { const { data } = useCurrentChatDetails(); const toggleBlockUser = useToggleBlockUser(); - if (!data || data.type !== "direct") return null; + if (!data || data.chat.type !== "direct") return null; return ( @@ -253,10 +308,10 @@ const ProfileDialog = () => {
- {data.picture && ( + {data.chat.picture && ( {data.name} @@ -265,10 +320,10 @@ const ProfileDialog = () => {
) : ( <> - {data.picture && ( + {data.chat.picture && ( {data.name} setIsFullScreenImage(true)} @@ -285,12 +340,14 @@ const ProfileDialog = () => { )}
- {data.name} + + {data.chat.name} +
- {data.otherUser?.status ? ( + {data.chat.otherUser?.status ? ( Online - ) : data.otherUser.lastSeen ? ( - `Last seen ${formatDistanceToNow(data.otherUser.lastSeen, { addSuffix: true })}` + ) : data.chat.otherUser.lastSeen ? ( + `Last seen ${formatDistanceToNow(data.chat.otherUser.lastSeen, { addSuffix: true })}` ) : ( "Offline" )} @@ -341,24 +398,26 @@ const ProfileDialog = () => {

Phone

-

{data.otherUser.phoneNumber || "Not available"}

+

{data.chat.otherUser.phoneNumber || "Not available"}

Status

-

{data.otherUser.status || "No status"}

+

{data.chat.otherUser.status || "No status"}

@@ -406,10 +465,10 @@ const GroupDialog = () => {
- {data.picture && ( + {data.chat.picture && ( {data.name} @@ -418,10 +477,10 @@ const GroupDialog = () => {
) : ( <> - {data.picture && ( + {data.chat.picture && ( {data.name} setIsFullScreenImage(true)} @@ -447,9 +506,11 @@ const GroupDialog = () => { )}
- {data.name} + + {data.chat.name} +
- {data.chatParticipants.length} participants + {data.chat.chatParticipants.length} participants
@@ -498,14 +559,14 @@ const GroupDialog = () => {

Description

-

{data.description}

+

{data.chat.description}

Created

-

{format(data.createdAt, "PPP")}

+

{format(data.chat.createdAt, "PPP")}

@@ -514,7 +575,7 @@ const GroupDialog = () => {

- {data.chatParticipants.length} Participants + {data.chat.chatParticipants.length} Participants

- - - - - + + + +
+ + + + Search Messages +
+ {/* {conversation && ( + + Searching in chat with {conversation.name} + + )} */} +
+ +
+
+
+ + setSearchTerm(e.target.value)} + /> + {searchTerm && ( + + )} +
+
+
+ + + {isPending ? ( +
+
+ Searching... +
+
+ ) : searchTerm && (data?.messages.length || 0) > 0 ? ( +
+
+ {data?.messages?.length}{" "} + {data?.messages?.length === 1 ? "result" : "results"} found +
+ + {data?.messages?.map((message) => ( +
handleMessageClick(message.id)} + > +
+ + {format(message.createdAt, "MMM d, yyyy • h:mm a")} +
+
+

+ {message.content + .split(new RegExp(`(${searchTerm})`, "gi")) + .map((part, i) => + part.toLowerCase() === searchTerm.toLowerCase() ? ( + + {part} + + ) : ( + part + ) + )} +

+
+
+ ))} +
+ ) : searchTerm ? ( +
+ +

+ No messages found matching {searchTerm} +

+
+ ) : ( +
+ +

+ Enter a search term to find messages +

+
+ )} +
+
+
); }; const ChatDropdown = () => { const { data } = useCurrentChatDetails(); const toggleBlockUser = useToggleBlockUser(); + const { setSearchOpen } = useSearchDrawer(); if (!data) return null; @@ -606,33 +802,28 @@ const ChatDropdown = () => { Chat Settings - - - e.preventDefault()}> - Search Messages - - - - + setSearchOpen(true)}> + Search Messages + e.preventDefault()}> - {data.type === "group" ? "View info" : "View profile"} + {data.chat.type === "group" ? "View info" : "View profile"} - {data.type === "group" ? : } + {data.chat.type === "group" ? : } - {data.type === "direct" && ( + {data.chat.type === "direct" && (
Add Friend toggleBlockUser.mutate()}> - {data.otherUser.isBlocked ? "Unblock User" : "Block User"} + {data.chat.otherUser.isBlocked ? "Unblock User" : "Block User"}
)} - {data.type === "group" ? "Leave Group" : "Delete Chat"} + {data.chat.type === "group" ? "Leave Group" : "Delete Chat"}
@@ -640,19 +831,23 @@ const ChatDropdown = () => { }; const MessagesList = () => { - const { userId } = useUser(); + const userId = useUserId(); const { chatId } = useCurrentChat(); const { data, fetchNextPage } = useInfiniteQuery({ queryKey: ["chatMessages", chatId], enabled: chatId !== null, queryFn: async ({ pageParam }) => { - const { status, data } = await api.chat[":chatId/messages"].get({ - path: { chatId: chatId!.toString() }, - query: { page: pageParam, limit: 50 }, + const { data } = await api.chat[":chatId/messages"].get({ + input: { + path: { chatId: chatId!.toString() }, + query: { page: pageParam, limit: 50 }, + }, + options: { + throwOnErrorStatus: true, + }, }); - if (status === "OK") return data; - throw new Error(data.error.message); + return data; }, initialPageParam: 1, getNextPageParam: (data) => { @@ -670,19 +865,19 @@ const MessagesList = () => { }, [inView, fetchNextPage]); const allMessages = React.useMemo( - () => data?.pages.flatMap((page) => page.data).reverse(), + () => data?.pages.flatMap((page) => page.messages).reverse(), [data] ); return ( ({ top: scrollArea.scrollHeight, behavior: "instant", })} > -
+
{allMessages?.map((message, index, messages) => { const isSentByUser = message.sender.id === userId; @@ -691,7 +886,7 @@ const MessagesList = () => { const prevDate = previous ? previous?.createdAt : undefined; return ( -
+
{ )} > -

{message.content}

+

{message.content}

-
+
{format(message.createdAt, "HH:mm")} @@ -731,8 +926,8 @@ const DateDivider = ({ if (!showDate) return null; return ( -
- +
+ {format(message.createdAt, "PPP")}
@@ -746,13 +941,13 @@ const MessageSender = ({ prevSenderId: number | undefined; message: ChatMessage; }) => { - const { userId } = useUser(); - const { data: chatData } = useCurrentChatDetails(); + const userId = useUserId(); + const { data } = useCurrentChatDetails(); const isSentByUser = message.sender.id === userId; const showSender = prevSenderId !== message.sender.id && - chatData?.type === "group" && + data?.chat?.type === "group" && !isSentByUser; if (!showSender) return null; @@ -761,7 +956,7 @@ const MessageSender = ({
- EL + {message.sender.name} {message.sender.name}
@@ -854,7 +1049,7 @@ const MessageAttachments = ({ message }: { message: ChatMessage }) => { }; const MessageReadReceipt = ({ message }: { message: ChatMessage }) => { - const { userId } = useUser(); + const userId = useUserId(); const isSentByUser = message.sender.id === userId; diff --git a/apps/web/src/app/(private)/(main)/_components/settings-drawer.tsx b/apps/web/src/app/(private)/(main)/_components/settings-drawer.tsx new file mode 100644 index 0000000..600debc --- /dev/null +++ b/apps/web/src/app/(private)/(main)/_components/settings-drawer.tsx @@ -0,0 +1,621 @@ +import React from "react"; +import { useRouter } from "next/navigation"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { + ArrowLeft, + User, + Phone, + Shield, + LogOut, + Trash2, + Camera, + Edit3, + Image, + Activity, + History, + CheckCheck, + PhoneCall, + Send, + AlignLeft, +} from "lucide-react"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetDescription, + SheetClose, +} from "@repo/ui/components/sheet"; +import { Input } from "@repo/ui/components/input"; +import { Button } from "@repo/ui/components/button"; +import { Label } from "@repo/ui/components/label"; +import { Textarea } from "@repo/ui/components/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@repo/ui/components/select"; +import { ScrollArea } from "@repo/ui/components/scroll-area"; +import { Separator } from "@repo/ui/components/separator"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@repo/ui/components/alert-dialog"; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@repo/ui/components/avatar"; +import { cn } from "@repo/ui/lib/utils"; +import useMediaQuery from "@repo/web/hooks/use-media-query"; +import api, { ApiInputs } from "@repo/web/api"; +import { useSettingsDrawer } from "../_providers/settings-drawer-provider"; +import { useUser } from "../_hooks/user-provider"; + +type UpdateUser = ApiInputs["/user"]["/"]["patch"]["body"]["user"]; +type UpdateUserPrivacySettings = + ApiInputs["/user"]["/privacy"]["patch"]["body"]["privacySettings"]; + +const useMutateUser = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (user: UpdateUser) => { + await api.user[""].patch({ + input: { + body: { user }, + }, + options: { + throwOnErrorStatus: true, + }, + }); + }, + onMutate: async (newUser) => { + await queryClient.cancelQueries({ queryKey: ["user"] }); + const previousUser = queryClient.getQueryData(["user"]); + queryClient.setQueryData(["user"], (oldData: UpdateUser) => ({ + ...oldData, + ...newUser, + })); + return { previousUser }; + }, + onSuccess: () => { + toast.success("Profile updated successfully!"); + }, + onError: (err, variables, context) => { + queryClient.setQueryData(["user"], context?.previousUser); + toast.error("Failed to update profile. Please try again."); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ["user"] }); + }, + }); +}; + +const useQueryPrivacySettings = () => { + return useQuery({ + queryKey: ["userPrivacySettings"], + queryFn: async () => { + const { data } = await api.user.privacy.get({ + options: { + throwOnErrorStatus: true, + }, + }); + return data; + }, + }); +}; + +const useMutatePrivacySettings = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (privacySettings: UpdateUserPrivacySettings) => { + await api.user.privacy.patch({ + input: { + body: { privacySettings }, + }, + options: { + throwOnErrorStatus: true, + }, + }); + }, + onMutate: async (newSettings) => { + await queryClient.cancelQueries({ queryKey: ["userPrivacySettings"] }); + const previousUserPrivacySettings = queryClient.getQueryData([ + "userPrivacySettings", + ]); + queryClient.setQueryData( + ["userPrivacySettings"], + (oldData: UpdateUserPrivacySettings) => ({ + ...oldData, + ...newSettings, + }) + ); + return { previousUserPrivacySettings }; + }, + onSuccess: () => { + toast.success("Privacy settings updated successfully!"); + }, + onError: (err, variables, context) => { + queryClient.setQueryData( + ["userPrivacySettings"], + context?.previousUserPrivacySettings + ); + toast.error("Failed to update privacy settings. Please try again."); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ["userPrivacySettings"] }); + }, + }); +}; + +const SettingsDrawer = () => { + const { isMobile } = useMediaQuery(); + const { settingsOpen, setSettingsOpen } = useSettingsDrawer(); + + return ( + + +
+ +
+ + + + + +
+
+ + + ); +}; + +const Header = () => { + return ( + +
+ + + + Settings +
+ + Manage your profile and privacy settings + +
+ ); +}; + +const ProfileSettings = () => { + const { data } = useUser(); + const [editingProfile, setEditingProfile] = React.useState(false); + const [tempProfile, setTempProfile] = React.useState<{ + name: string; + description: string; + picture?: File | undefined; + }>({ + name: "", + description: "", + picture: undefined, + }); + React.useEffect(() => { + if (data?.user) { + setTempProfile({ + name: data.user.name ?? "", + description: data.user.description ?? "", + picture: undefined, + }); + } + }, [data]); + const updateUser = useMutateUser(); + + const handleSaveProfile = () => { + setEditingProfile(false); + updateUser.mutate(tempProfile); + }; + + const handleCancelEdit = () => { + setTempProfile({ + name: data?.user.name || "", + description: data?.user.description || "", + picture: undefined, + }); + setEditingProfile(false); + }; + + return ( +
+
+

+ + Profile +

+
+ +
+
+ +
+
+ + + {data?.user.name} + + {editingProfile && ( + + )} +
+
+ {editingProfile ? ( +
+ + + setTempProfile((prev) => ({ + ...prev, + name: e.target.value, + })) + } + placeholder="Your name" + /> +
+ ) : ( +
+

{data?.user.name}

+

+ {data?.user.description} +

+
+ )} +
+
+ + {editingProfile && ( +
+
+ +