Skip to content

Commit

Permalink
[Feature][Antonio] Added chat messaging to all app users
Browse files Browse the repository at this point in the history
  • Loading branch information
antonioken22 committed Jul 16, 2024
1 parent 645761e commit 1a73be7
Show file tree
Hide file tree
Showing 12 changed files with 632 additions and 30 deletions.
80 changes: 52 additions & 28 deletions app/(routes)/_components/notification-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ const NotificationDropdown = () => {
));
};

// Format the dateCreated field in the format (mm-dd-yyyy @ hh:mm am/pm)
const formattedDate = (dateCreated: Timestamp | null) => {
if (!dateCreated) return "";
const date = dateCreated.toDate();
return date.toLocaleString("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: true,
});
};

return (
<Popover>
<PopoverTrigger asChild>
Expand All @@ -108,38 +122,48 @@ const NotificationDropdown = () => {
<div className="divide-y divide-muted max-h-80 overflow-y-auto">
{userNotifications.length > 0 ? (
userNotifications.map((notification: Notification) => (
<div
key={notification.id}
className={`flex justify-between items-center py-2 text-xs md:text-sm ${
notification.isRead ? "text-muted-foreground" : "font-normal"
}`}
>
<span>{replaceNlWithNewLine(notification.body || "")}</span>
<div className="flex flex-col items-center space-y-1 ml-1">
<div className="flex flex-col items-center">
<div
role="button"
onClick={() => handleMarkAsRead(notification.id || "")}
className="p-2"
>
<CheckCircle className="w-4 h-4" />
<>
<div
key={notification.id}
className={`flex justify-between items-center py-2 text-xs md:text-sm ${
notification.isRead
? "text-muted-foreground"
: "font-normal"
}`}
>
<span>{replaceNlWithNewLine(notification.body || "")}</span>
<div className="flex flex-col items-center space-y-1 ml-1">
<div className="flex flex-col items-center">
<div
role="button"
onClick={() => handleMarkAsRead(notification.id || "")}
className="p-2"
>
<CheckCircle className="w-4 h-4" />
</div>
<span className="text-xs text-muted">Read</span>
</div>
<span className="text-xs text-muted">Read</span>
</div>
<div className="flex flex-col items-center">
<div
role="button"
onClick={() =>
handleRemoveFromView(notification.id || "")
}
className="p-2"
>
<Trash2 className="w-4 h-4" />
<div className="flex flex-col items-center">
<div
role="button"
onClick={() =>
handleRemoveFromView(notification.id || "")
}
className="p-2"
>
<Trash2 className="w-4 h-4" />
</div>
<span className="text-xs text-muted">Delete</span>
</div>
<span className="text-xs text-muted">Delete</span>
</div>
</div>
</div>
<h3 className="flex flex-col text-xs text-muted-foreground text-right pr-2 mb-2">
Received at:{" "}
{formattedDate(
notification.dateCreated as unknown as Timestamp
)}
</h3>
</>
))
) : (
<div
Expand Down
7 changes: 7 additions & 0 deletions app/(routes)/_components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
SquareParking,
Ticket,
Car,
MessageSquare,
} from "lucide-react";
import { usePathname } from "next/navigation";
import { signOut } from "firebase/auth";
Expand Down Expand Up @@ -41,6 +42,12 @@ const routes = [
color: "text-primary",
href: "/dashboard",
},
{
label: "Chat",
icon: MessageSquare,
color: "text-primary",
href: "/chat",
},
{
label: "Booking",
icon: Ticket,
Expand Down
68 changes: 68 additions & 0 deletions app/(routes)/chat/_components/chat-compose-message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useState } from "react";
import { Send } from "lucide-react";
import { toast } from "sonner";

import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import useUserState from "@/hooks/useUserState";
import useChatMessages from "@/hooks/useChatMessages";
import { ChatMessage } from "@/types/ChatMessage";

interface ChatComposeMessageProps {
selectedUser: {
userId: string;
userFirstName: string;
userLastName: string;
userPhotoUrl: string | null;
} | null;
}

const ChatComposeMessage: React.FC<ChatComposeMessageProps> = ({
selectedUser,
}) => {
const { userId, userFirstName, userLastName } = useUserState();
const { addChatMessage } = useChatMessages();
const [message, setMessage] = useState("");

const handleSend = async () => {
if (message.trim() && selectedUser) {
const newChatMessage: ChatMessage = {
message,
isView: true,
sender: {
userId: userId || "",
userFirstName: userFirstName || "",
userLastName: userLastName || "",
},
recipient: {
userId: selectedUser.userId,
userFirstName: selectedUser.userFirstName,
userLastName: selectedUser.userLastName,
},
};
await addChatMessage(newChatMessage);
setMessage("");
} else if (!selectedUser) {
toast.warning("No user selected.");
}
};

return (
<div className="flex items-center space-x-2 p-2 bg-background">
<Textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type your message..."
className="flex-grow border-primary"
/>
<Button
onClick={handleSend}
className="h-20 flex items-center justify-center"
>
<Send className="w-5 h-5" />
</Button>
</div>
);
};

export default ChatComposeMessage;
130 changes: 130 additions & 0 deletions app/(routes)/chat/_components/chat-message-history.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import React, { useEffect, useRef } from "react";
import { Timestamp } from "firebase/firestore";
import { Trash } from "lucide-react";

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import useUserState from "@/hooks/useUserState";
import useChatMessages from "@/hooks/useChatMessages";
import { ChatMessage } from "@/types/ChatMessage";

interface ChatMessageHistoryProps {
selectedUser: {
userId: string;
userFirstName: string;
userLastName: string;
userPhotoUrl: string | null;
} | null;
}

const ChatMessageHistory: React.FC<ChatMessageHistoryProps> = ({
selectedUser,
}) => {
const { userId, userFirstName, userLastName, userPhotoUrl } = useUserState();
const { chatMessages, removeFromView } = useChatMessages();
const messagesEndRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
// Scroll to the bottom when messages change
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [chatMessages]);

if (!selectedUser) {
return (
<div className="text-center text-muted-foreground py-4">
Select a user to start chatting.
</div>
);
}

const filteredMessages = chatMessages
.filter(
(msg: ChatMessage) =>
(msg.sender.userId === userId &&
msg.recipient.userId === selectedUser.userId) ||
(msg.sender.userId === selectedUser.userId &&
msg.recipient.userId === userId)
)
.sort((a, b) => {
const dateA = (a.dateCreated as unknown as Timestamp)?.seconds ?? 0;
const dateB = (b.dateCreated as unknown as Timestamp)?.seconds ?? 0;
return dateA - dateB; // Sort in ascending order (oldest on top)
});

return (
<div className="p-4 space-y-4">
{filteredMessages.length === 0 ? (
<div className="text-center text-muted-foreground py-4">
Start a conversation
</div>
) : (
filteredMessages.map((msg) => {
const isCurrentUser = msg.sender.userId === userId;
return (
<div
key={msg.id}
className={`flex ${
isCurrentUser ? "justify-end" : "justify-start"
}`}
>
{!isCurrentUser && (
<Avatar className="w-10 h-10 border border-muted-foreground mr-2">
<AvatarImage src={selectedUser?.userPhotoUrl ?? undefined} />
<AvatarFallback>
{selectedUser?.userFirstName?.[0] ?? "U"}
</AvatarFallback>
</Avatar>
)}
<div
className={`flex flex-col ${
isCurrentUser ? "items-end" : "items-start"
}`}
>
<div
className={`relative ${
isCurrentUser
? "border border-primary bg-muted text-primary"
: "border border-muted-foreground bg-muted text-muted-foreground"
} p-2 rounded-lg`}
>
<div className="text-sm md:text-base font-bold">
{isCurrentUser
? `${userFirstName ?? "Unknown"} ${
userLastName ?? "User"
}`
: `${selectedUser.userFirstName} ${selectedUser.userLastName}`}
</div>
<div className="text-sm md:text-base">
{msg.isView ? (
msg.message
) : (
<i className="text-xs">Message deleted</i>
)}
</div>
{isCurrentUser && msg.isView && (
<button
className="absolute -top-0 -left-6"
onClick={() => removeFromView(msg.id || "")}
>
<Trash className="w-4 h-4 text-muted-foreground" />
</button>
)}
</div>
</div>
{isCurrentUser && (
<Avatar className="w-10 h-10 border border-primary ml-2">
<AvatarImage src={userPhotoUrl ?? undefined} />
<AvatarFallback>{userFirstName?.[0] ?? "U"}</AvatarFallback>
</Avatar>
)}
</div>
);
})
)}
<div ref={messagesEndRef} />
</div>
);
};

export default ChatMessageHistory;
29 changes: 29 additions & 0 deletions app/(routes)/chat/_components/chat-ui.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { useState } from "react";
import ChatUsers from "./chat-users";
import ChatMessageHistory from "./chat-message-history";
import ChatComposeMessage from "./chat-compose-message";

const ChatUI = () => {
const [selectedUser, setSelectedUser] = useState<{
userId: string;
userFirstName: string;
userLastName: string;
userPhotoUrl: string | null;
} | null>(null);

return (
<div className="flex flex-col">
<ChatUsers onSelectUser={setSelectedUser} />
<hr />
<div className="h-[348px] md:h-[594px] overflow-y-auto ">
<ChatMessageHistory selectedUser={selectedUser} />
</div>
<div className="fixed left-2 md:left-56 md:ml-2 bottom-0 right-2 ">
<hr />
<ChatComposeMessage selectedUser={selectedUser} />
</div>
</div>
);
};

export default ChatUI;
Loading

0 comments on commit 1a73be7

Please sign in to comment.