From 92c013ad0823eb73c9c2f853fd69e1bb133b7e2d Mon Sep 17 00:00:00 2001 From: Justin Xiao Date: Thu, 20 Jul 2023 21:13:28 +0800 Subject: [PATCH] feat: extract input and optimize ui --- README.md | 8 ++ frontend/src/app/home/page.tsx | 105 ++++++++++------------ frontend/src/app/layout.tsx | 4 +- frontend/src/components/Avatar.tsx | 69 ++++++++++++++ frontend/src/components/ChatroomInput.tsx | 62 +++++++++++++ frontend/src/components/ChatroomTitle.tsx | 58 +++--------- 6 files changed, 200 insertions(+), 106 deletions(-) create mode 100644 frontend/src/components/Avatar.tsx create mode 100644 frontend/src/components/ChatroomInput.tsx diff --git a/README.md b/README.md index 5d1e706..3101a2f 100644 --- a/README.md +++ b/README.md @@ -1 +1,9 @@ # nextjs13-socketio-boilerplate + +[![License MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](./LICENSE) +Made with TypeScript +Powered by Vercel + + +## Project Description +This boilerplate \ No newline at end of file diff --git a/frontend/src/app/home/page.tsx b/frontend/src/app/home/page.tsx index 14e20f3..fb3e510 100644 --- a/frontend/src/app/home/page.tsx +++ b/frontend/src/app/home/page.tsx @@ -4,24 +4,18 @@ import { Card, Container, Text, - Divider, ScrollArea, - TextInput, - ActionIcon, } from "@mantine/core"; -import { IconArrowRight } from "@tabler/icons-react"; import useSocketStore from "@/store/socket"; import { useEffect, useRef, useState, useLayoutEffect } from "react"; import { MessageWithMe, SocketMessage } from "@/types/next"; -import { toast } from "react-hot-toast"; import ChatroomTitle from "@/components/ChatroomTitle"; +import ChatroomInput from "@/components/ChatroomInput"; const useStyles = createStyles((theme) => ({ rightMessageField: { display: "flex", flexDirection: "row-reverse", - flexWrap: "nowrap", - // alignItems: "center", width: "100%", marginTop: theme.spacing.xs, marginBottom: theme.spacing.xs, @@ -29,14 +23,23 @@ const useStyles = createStyles((theme) => ({ rightMessage: { width: "fit-content", padding: theme.spacing.xs, - borderRadius: theme.radius.md, + overflowWrap: "break-word", + borderRadius: theme.radius.lg, backgroundColor: theme.colors.green[2], + maxWidth: "50em", + [theme.fn.smallerThan("md")]: { + maxWidth: "35em", + }, + [theme.fn.smallerThan("sm")]: { + maxWidth: "25em", + }, + [theme.fn.smallerThan("xs")]: { + maxWidth: "15em", + }, }, leftMessageField: { display: "flex", flexDirection: "row", - flexWrap: "nowrap", - // alignItems: "center", width: "100%", marginTop: theme.spacing.xs, marginBottom: theme.spacing.xs, @@ -44,11 +47,25 @@ const useStyles = createStyles((theme) => ({ leftMessage: { width: "fit-content", padding: theme.spacing.xs, - borderRadius: theme.radius.md, + overflowWrap: "break-word", + borderRadius: theme.radius.lg, backgroundColor: theme.colors.gray[2], + maxWidth: "50em", + [theme.fn.smallerThan("md")]: { + maxWidth: "35em", + }, + [theme.fn.smallerThan("sm")]: { + maxWidth: "25em", + }, + [theme.fn.smallerThan("xs")]: { + maxWidth: "15em", + }, }, + timestamp: { + width: "fit-content", display: "flex", + flexWrap: "nowrap", fontSize: theme.fontSizes.xs, color: theme.colors.gray[5], marginLeft: theme.spacing.xs, @@ -58,31 +75,17 @@ const useStyles = createStyles((theme) => ({ })); export default function Home() { - const { socket, connect, emit } = useSocketStore((state) => state); // deconstructing socket and its method from socket store - - const chatViewportRef = useRef(null); // binding chat viewport ref to scroll to bottom - const messageInputRef = useRef(null); // binding message input ref to focus - - const [targetSocketId, setTargetSocketId] = useState(""); // target socket id input value - const [message, setMessage] = useState(""); // message input value - const [messages, setMessages] = useState([]); // show messages on ScrollArea - const { classes } = useStyles(); - + const { socket, connect } = useSocketStore((state) => state); // deconstructing socket and its method from socket store + const chatViewportRef = useRef(null); // binding chat viewport ref to scroll to bottom + const [targetSocketId, setTargetSocketId] = useState(""); // target socket id input value + const [messages, setMessages] = useState([]); // show messages on ScrollArea const scrollToBottom = () => { chatViewportRef?.current?.scrollTo({ top: chatViewportRef.current.scrollHeight, behavior: "smooth", }); }; - const sendMessage = () => { - 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(); - }; useEffect(() => { connect(); @@ -106,21 +109,22 @@ export default function Home() { return ( <> - - - - - + + + + + + {messages.map((message, index) => { return (
@@ -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 ( + +