Skip to content

Commit

Permalink
Fixing some styling problems and add support for mobile
Browse files Browse the repository at this point in the history
  • Loading branch information
FunkyOz committed Dec 27, 2024
1 parent 51a37cf commit bf042fb
Show file tree
Hide file tree
Showing 19 changed files with 160 additions and 51 deletions.
6 changes: 1 addition & 5 deletions apps/site/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,7 @@ function App() {

return (
<div className="h-screen w-screen flex flex-col bg-white overflow-hidden">
<Chat
classNames={{
container: "bg-black",
}}
>
<Chat>
<Sidebar items={conversations}>
{(item, key) => (
<SidebarItem
Expand Down
55 changes: 41 additions & 14 deletions lib/components/chat/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,58 @@
import React from "react";
import { ChatProps } from "../../types";
import { ChatWrapper, ChatContent, ChatContainer } from "./styles/chat.styles";
import {
ChatWrapper,
ChatContent,
ChatContainer,
ChatLayer,
} from "./styles/chat.styles";
import { useChat } from "./hooks/useChat";
import { ChatProvider } from "../../provider";
import { ChatProvider, useChatProvider } from "../../provider";
import useClassNames from "../../hooks/useClassNames";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import { useSidebarHandler } from "../sidebar/hooks/useSidebarHandler";

export const Chat: React.FC<ChatProps> = ({
children,
className,
classNames,
}) => {
const { sidebar, messageInput, messages } = useChat(children);
const classes = useClassNames({ className, classNames });

return (
<ChatProvider>
<ChatWrapper className={classes?.base}>
<ChatContent className={classes?.content}>
{sidebar}
<ChatContainer className={classes?.container}>
{messages}
{messageInput}
</ChatContainer>
</ChatContent>
</ChatWrapper>
<ChatHelper className={className} classNames={classNames}>
{children}
</ChatHelper>
</ChatProvider>
);
};

export default Chat;

const ChatHelper: React.FC<ChatProps> = ({
children,
className,
classNames,
}) => {
const { sidebar, messageInput, messages } = useChat(children);
const classes = useClassNames({ className, classNames });
const isMobile = useMediaQuery("(max-width: 768px)");
const { handleClose } = useSidebarHandler();
const {
state: { isSidebarOpen },
} = useChatProvider();

return (
<ChatWrapper className={classes?.base}>
<ChatContent className={classes?.content}>
{sidebar}
{isMobile && isSidebarOpen && (
<ChatLayer onClick={handleClose} />
)}
<ChatContainer className={classes?.container}>
{messages}
{messageInput}
</ChatContainer>
</ChatContent>
</ChatWrapper>
);
};
19 changes: 16 additions & 3 deletions lib/components/chat/styles/chat.styles.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import styled from "styled-components";
import { withDevClassName } from "../../../utils";

export const ChatWrapper = styled.div`
export const ChatWrapper = styled.div.attrs(withDevClassName("chat-wrapper"))`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
`;

export const ChatContent = styled.div`
export const ChatContent = styled.div.attrs(withDevClassName("chat-content"))`
display: flex;
flex: 1;
overflow: hidden;
`;

export const ChatContainer = styled.div`
export const ChatContainer = styled.div.attrs(
withDevClassName("chat-container")
)`
display: flex;
flex-direction: column;
flex: 1;
Expand All @@ -22,3 +25,13 @@ export const ChatContainer = styled.div`
overflow-y: auto;
background-color: #ffffff;
`;

export const ChatLayer = styled.div.attrs(withDevClassName("chat-layer"))`
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
z-index: 20;
background-color: rgba(0, 0, 0, 0.4);
`;
8 changes: 4 additions & 4 deletions lib/components/message/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ import { useScrollToBottom } from "./hooks/useScrollToBottom";
import useClassNames from "../../hooks/useClassNames";
import { useChatProvider } from "../../provider";

export const Messages = <T extends object>({
export function Messages<T>({
items = [],
children,
loadingContent,
isLoading = false,
className,
classNames,
headerContent,
}: MessagesProps<T>) => {
const bottomRef = useScrollToBottom([children, isLoading]);
}: MessagesProps<T>) {
const bottomRef = useScrollToBottom([items, isLoading]);
const {
state: { isSidebarOpen },
} = useChatProvider();
Expand Down Expand Up @@ -55,6 +55,6 @@ export const Messages = <T extends object>({
<BottomHelper ref={bottomRef} />
</MessagesWrapper>
);
};
}

Messages.displayName = "Messages";
3 changes: 3 additions & 0 deletions lib/components/message/styles/messages.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ const BaseMessageWrapper = styled.div.attrs(
align-items: flex-start;
gap: 0.5rem;
max-width: 45rem;
width: 100%;
margin: 0 auto;
padding: 0 1rem;
`;

export const UserMessageWrapper = styled(BaseMessageWrapper).attrs(
Expand Down Expand Up @@ -62,6 +64,7 @@ export const AssistantMessageContent = styled(BaseMessageContent).attrs(
withDevClassName("assistant-message-content")
)`
color: #000000;
white-space: normal;
`;

const pulse = keyframes`
Expand Down
9 changes: 0 additions & 9 deletions lib/components/sidebar/hooks/useSidebar.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { ReactNode, isValidElement, useMemo } from "react";
import { useChatProvider } from "../../../provider";
import { SidebarItem } from "../sidebar-item";
import { mRound } from "../../../utils";

type UseSidebarReturn = {
renderItems: () => ReactNode;
handleToggle: () => any;
size: number;
};

Expand All @@ -25,8 +23,6 @@ export const useSidebar = <T>({
children,
size,
}: UseSidebarProps<T>): UseSidebarReturn => {
const { dispatch } = useChatProvider();

const remSize = useMemo(
() => (size ? mRound(size / 16, 0.25) : 16),
[size]
Expand Down Expand Up @@ -54,13 +50,8 @@ export const useSidebar = <T>({
return children as ReactNode;
};

const handleToggle = () => {
dispatch({ type: "TOGGLE_SIDEBAR" });
};

return {
size: remSize,
renderItems,
handleToggle,
};
};
19 changes: 19 additions & 0 deletions lib/components/sidebar/hooks/useSidebarHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useChatProvider } from "../../../provider";

export const useSidebarHandler = () => {
const { dispatch } = useChatProvider();

const handleToggle = () => {
dispatch({ type: "TOGGLE_SIDEBAR" });
};

const handleOpen = () => {
dispatch({ type: "OPEN_SIDEBAR" });
};

const handleClose = () => {
dispatch({ type: "CLOSE_SIDEBAR" });
};

return { handleToggle, handleOpen, handleClose };
};
7 changes: 6 additions & 1 deletion lib/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { useSidebar } from "./hooks/useSidebar";
import { MenuIcon } from "../icons";
import { useChatProvider } from "../../provider";
import useClassNames from "../../hooks/useClassNames";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import { useSidebarHandler } from "./hooks/useSidebarHandler";

export const Sidebar = <T extends object>({
items = [],
Expand All @@ -23,15 +25,17 @@ export const Sidebar = <T extends object>({
className,
classNames,
}: SidebarProps<T>) => {
const { renderItems, handleToggle, size } = useSidebar({
const { renderItems, size } = useSidebar({
items,
children,
size: pixelSize,
});
const { handleToggle } = useSidebarHandler();
const {
state: { isSidebarOpen },
} = useChatProvider();
const classes = useClassNames({ className, classNames });
const isMobile = useMediaQuery("(max-width: 768px)");

return (
<SidebarWrapper className={classes.base}>
Expand All @@ -48,6 +52,7 @@ export const Sidebar = <T extends object>({
className={classes.container}
$isOpen={isSidebarOpen}
$size={size}
$isMobile={isMobile}
>
<SidebarContent className={classes.content} $size={size}>
<SidebarHeader
Expand Down
17 changes: 14 additions & 3 deletions lib/components/sidebar/styles/sidebar.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,25 @@ export const SidebarWrapper = styled.div.attrs(

export const SidebarContainer = styled.div.attrs(
withDevClassName("sidebar-container")
)<{ $isOpen?: boolean; $size?: number }>`
)<{ $isOpen?: boolean; $size?: number; $isMobile?: boolean }>`
width: ${({ $isOpen, $size = 16 }) => ($isOpen ? `${$size}rem` : "0")};
background: #f9f9f9;
overflow: hidden;
transition: width 0.3s ease;
transition: width ${({ $isMobile }) => ($isMobile ? "0.1s" : "0.3s")} ease;
display: flex;
flex-direction: column;
height: 100%;
${({ $isMobile }) =>
$isMobile &&
`
position: absolute;
z-index: 30;
top: 0;
left: 0;
right: 0;
bottom: 0;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
`}
`;

export const SidebarContent = styled.div.attrs(
Expand Down Expand Up @@ -70,7 +81,7 @@ export const ToggleButton = styled.button.attrs(
position: absolute;
top: 0.75rem;
left: 1rem;
z-index: 20;
z-index: 40;
transform: ${({ $isOpen }) =>
$isOpen ? "rotate(-180deg)" : "rotate(0deg)"};
transition: all 0.3s ease;
Expand Down
21 changes: 21 additions & 0 deletions lib/hooks/useMediaQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from "react";

export function useMediaQuery(query: string) {
const subscribe = React.useCallback(
(callback: () => void) => {
const matchMedia = window.matchMedia(query);

matchMedia.addEventListener("change", callback);
return () => {
matchMedia.removeEventListener("change", callback);
};
},
[query]
);

const getSnapshot = () => {
return window.matchMedia(query).matches;
};

return React.useSyncExternalStore(subscribe, getSnapshot);
}
7 changes: 6 additions & 1 deletion lib/provider/chat-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { createContext, useContext, useReducer, ReactNode } from "react";
import { ChatProviderContextType } from "./types";
import { chatReducer, initialState } from "./reducer";
import { useMediaQuery } from "../hooks/useMediaQuery";

const ChatContext = createContext<ChatProviderContextType | undefined>(
undefined
Expand All @@ -19,7 +20,11 @@ type ChatProviderProps = {
};

export const ChatProvider: React.FC<ChatProviderProps> = ({ children }) => {
const [state, dispatch] = useReducer(chatReducer, initialState);
const isMobile = useMediaQuery("(max-width: 768px)");
const [state, dispatch] = useReducer(chatReducer, {
...initialState,
isSidebarOpen: !isMobile,
});

return (
<ChatContext.Provider value={{ state, dispatch }}>
Expand Down
10 changes: 10 additions & 0 deletions lib/provider/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ export const chatReducer = (
...state,
isSidebarOpen: !state.isSidebarOpen,
};
case "OPEN_SIDEBAR":
return {
...state,
isSidebarOpen: true,
};
case "CLOSE_SIDEBAR":
return {
...state,
isSidebarOpen: false,
};
default:
return state;
}
Expand Down
5 changes: 4 additions & 1 deletion lib/provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ export type ChatState = {
isSidebarOpen: boolean;
};

export type ChatAction = { type: "TOGGLE_SIDEBAR" };
export type ChatAction =
| { type: "TOGGLE_SIDEBAR" }
| { type: "OPEN_SIDEBAR" }
| { type: "CLOSE_SIDEBAR" };

export type ChatProviderContextType = {
state: ChatState;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@funkyoz/react-chat",
"version": "1.0.3",
"version": "1.0.4",
"type": "module",
"main": "dist/main.js",
"types": "dist/main.d.ts",
Expand Down
5 changes: 4 additions & 1 deletion tests/components/chat/chat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { Sidebar } from "../../../lib/components/sidebar/sidebar";
import { MessageInput } from "../../../lib/components/message/message-input";
import { Messages } from "../../../lib/components/message/messages";

// Mock the components
vi.mock("../../../lib/hooks/useMediaQuery", () => ({
useMediaQuery: vi.fn(() => false),
}));

vi.mock("../../../lib/components/sidebar/sidebar", () => ({
Sidebar: ({
children,
Expand Down
Loading

0 comments on commit bf042fb

Please sign in to comment.