@@ -138,26 +142,9 @@ export default function Home() {
);
})}
-
-
-
setMessage(e.target.value)}
- mt={10}
- radius="xl"
- size="md"
- rightSection={
-
-
-
- }
- placeholder="Type something..."
- rightSectionWidth={42}
- onKeyDown={(e) => {
- if (e.key === "Enter") sendMessage();
- }}
- />
+
+
+
>
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 9ca74b9..5f5bb35 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -10,8 +10,8 @@ type Prop = {
};
export const metadata = {
- title: "Tiny Socket.io demo",
- description: "This repo implements a simple chat app with Socket.io and Next.js 13.",
+ title: "A Simple Full-Stack Socket.io Demo",
+ description: "This repo implements a simple chat app with Socket.io, Next.js 13, Mantine and Zustand.",
};
export default function RootLayout({ children }: Prop) {
diff --git a/frontend/src/components/Avatar.tsx b/frontend/src/components/Avatar.tsx
new file mode 100644
index 0000000..6ad157a
--- /dev/null
+++ b/frontend/src/components/Avatar.tsx
@@ -0,0 +1,69 @@
+import { createStyles, Indicator, Overlay, Avatar as MantineAvatar } from "@mantine/core";
+import { IconPencil } from "@tabler/icons-react";
+import React, { useEffect, useState } from "react";
+import NameModal from "./NameModal";
+import useBasicStore from "@/store/basic";
+import { useDisclosure } from "@mantine/hooks";
+import useSocketStore from "@/store/socket";
+
+const useStyles = createStyles((theme, { isShown }: { isShown: boolean }) => ({
+ overlay: {
+ cursor: "pointer",
+ opacity: !isShown ? "0" : "1",
+ transition: "all .2s",
+ visibility: !isShown ? "hidden" : "visible",
+ },
+}));
+
+const Avatar = () => {
+ const socket = useSocketStore((state) => state.socket); // deconstructing socket and its method from socket store
+ const storeName = useBasicStore((state) => state.name); // get name from basic store
+ const [name, setName] = useState(null); // avoiding Next.js hydration error
+ const [modalOpened, { open: modalOpen, close: modalClose }] = useDisclosure(false); // control change name modal open/close
+ const [isShown, setIsShown] = useState(false);
+ const { classes } = useStyles({ isShown });
+
+ useEffect(() => {
+ setName(storeName);
+ if (!storeName) modalOpen();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [storeName]);
+
+ return (
+ <>
+
+ setIsShown(true)}
+ onMouseLeave={() => setIsShown(false)}
+ >
+ {name && name.length > 3 ? `${name.slice(0, 1)}` : name}
+
+
+
+
+
+
+ >
+ );
+};
+
+export default Avatar;
diff --git a/frontend/src/components/ChatroomInput.tsx b/frontend/src/components/ChatroomInput.tsx
new file mode 100644
index 0000000..4f95e4f
--- /dev/null
+++ b/frontend/src/components/ChatroomInput.tsx
@@ -0,0 +1,62 @@
+import useSocketStore from "@/store/socket";
+import { ActionIcon, Group, Textarea, createStyles } from "@mantine/core";
+import { IconSend } from "@tabler/icons-react";
+import { KeyboardEvent, FC, useRef, useState } from "react";
+import toast from "react-hot-toast";
+
+type Props = {
+ targetSocketId: string;
+};
+const useStyles = createStyles((theme) => ({
+ inputWithoutBorder: {
+ border: "none",
+ },
+}));
+const ChatroomInput: FC = ({ targetSocketId }) => {
+ const { socket, emit } = useSocketStore((state) => state); // deconstructing socket and its method from socket store
+ const [message, setMessage] = useState(""); // message input value
+ const messageInputRef = useRef(null); // binding message input ref to focus
+ const { classes } = useStyles();
+
+ const sendMessage = () => {
+ console.log("sendMessage", message === "");
+ if (!message) return toast.error("Please enter a message");
+ if (!socket?.connected) return toast.error("Please reconnect server first");
+ if (!targetSocketId) return toast.error("Please enter a target socket id");
+ emit("message", { from: socket?.id, to: targetSocketId, timestamp: Date.now(), message });
+ setMessage("");
+ messageInputRef.current?.focus();
+ };
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Enter" && e.altKey !== true && e.shiftKey !== true && e.ctrlKey !== true) {
+ e.preventDefault();
+ sendMessage();
+ }
+ };
+
+ return (
+
+
+ );
+};
+
+export default ChatroomInput;
diff --git a/frontend/src/components/ChatroomTitle.tsx b/frontend/src/components/ChatroomTitle.tsx
index cc47a34..b7a7439 100644
--- a/frontend/src/components/ChatroomTitle.tsx
+++ b/frontend/src/components/ChatroomTitle.tsx
@@ -7,7 +7,7 @@ import {
Popover,
CopyButton,
Tooltip,
- Avatar,
+ Avatar as MantineAvatar,
Indicator,
Menu,
} from "@mantine/core";
@@ -15,18 +15,15 @@ import {
IconPlug,
IconCheck,
IconCopy,
- IconEdit,
IconPlugOff,
IconChevronDown,
IconUserCog,
IconUser,
} from "@tabler/icons-react";
import { SetStateAction, Dispatch, FC, useEffect, useState } from "react";
-import NameModal from "./NameModal";
-import useBasicStore from "@/store/basic";
-import { useDisclosure } from "@mantine/hooks";
import useSocketStore from "@/store/socket";
import { environment } from "@/config";
+import Avatar from "./Avatar";
type Props = {
targetSocketId: string;
@@ -35,18 +32,10 @@ type Props = {
const ChatroomTitle: FC = ({ targetSocketId, setTargetSocketId }) => {
const { socket, connect, disconnect } = useSocketStore(); // deconstructing socket and its method from socket store
- const storeName = useBasicStore((state) => state.name); // get name from basic store
- const [name, setName] = useState(null); // avoiding Next.js hydration error
- const [modalOpened, { open: modalOpen, close: modalClose }] = useDisclosure(false); // control change name modal open/close
+
const [popoverOpened, setPopoverOpened] = useState(false); // control popover open/close
const [onlineUsers, setOnlineUsers] = useState>({}); // online users
- useEffect(() => {
- setName(storeName);
- if (!storeName) modalOpen();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [storeName]);
-
useEffect(() => {
socket?.on("online_user", (onlineUsers: Record) => {
setOnlineUsers(onlineUsers);
@@ -59,20 +48,9 @@ const ChatroomTitle: FC = ({ targetSocketId, setTargetSocketId }) => {
return (
<>
-
-
-
-
- {name}
-
-
+
+
+
= ({ targetSocketId, setTargetSocketId }) => {
Actions
-
- {
- modalOpen();
- setPopoverOpened(false);
- }}
- >
-
-
-
{
// if socket is not connected, show connect button
socket?.connected ? (
@@ -151,11 +118,11 @@ const ChatroomTitle: FC = ({ targetSocketId, setTargetSocketId }) => {
-
+
To:
setTargetSocketId(e.currentTarget.value)}
/>
@@ -168,7 +135,9 @@ const ChatroomTitle: FC = ({ targetSocketId, setTargetSocketId }) => {
- {environment === "development" ? "Not available in development" : "Online user"}
+ {environment === "development"
+ ? "Not available in development"
+ : "Online user"}
{socket?.connected &&
Object.keys(onlineUsers)
@@ -190,7 +159,7 @@ const ChatroomTitle: FC = ({ targetSocketId, setTargetSocketId }) => {
color="teal"
withBorder
>
- = ({ targetSocketId, setTargetSocketId }) => {
w="fit-content"
>
-
+
{onlineUsers[socketId]}
@@ -208,7 +177,6 @@ const ChatroomTitle: FC = ({ targetSocketId, setTargetSocketId }) => {
-
>
);
};