mirror of
https://github.com/StepanovPlaton/Chat.git
synced 2026-04-04 04:40:42 +04:00
Complete project
This commit is contained in:
@@ -1,14 +1,32 @@
|
||||
@tailwind base;
|
||||
/* @tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind utilities; */
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
font-size: calc((100vw / 1920) * 20);
|
||||
|
||||
--color-col1: #fb4934;
|
||||
--color-col2: #b8bb26;
|
||||
--color-col3: #fabd2f;
|
||||
--color-col4: #83a598;
|
||||
--color-col5: #d3869b;
|
||||
--color-col6: #8ec07c;
|
||||
--color-col7: #a89986;
|
||||
--color-col8: #fe8019;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-bg0: #fbf1c7;
|
||||
--color-bg1: #ebdbb2;
|
||||
--color-bg2: #d5c4a1;
|
||||
--color-bg3: #bdae93;
|
||||
--color-bg4: #a89984;
|
||||
|
||||
--color-fg0: #282828;
|
||||
--color-fg1: #3c3836;
|
||||
--color-fg2: #504945;
|
||||
--color-fg3: #665c54;
|
||||
--color-fg4: #7c6f64;
|
||||
|
||||
--color-ac0: #83a598;
|
||||
@@ -16,18 +34,19 @@
|
||||
--color-ac2: #8ec07c;
|
||||
|
||||
--color-err: #cc241d;
|
||||
|
||||
--app-width: 70%;
|
||||
font-size: calc((100vw / 1920) * 20);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--color-bg0: #282828;
|
||||
--color-bg1: #3c3836;
|
||||
--color-bg2: #504945;
|
||||
--color-bg3: #665c54;
|
||||
--color-bg4: #7c6f64;
|
||||
|
||||
--color-fg0: #fbf1c7;
|
||||
--color-fg1: #ebdbb2;
|
||||
--color-fg2: #d5c4a1;
|
||||
--color-fg3: #bdae93;
|
||||
--color-fg4: #a89984;
|
||||
|
||||
--color-ac0: #076678;
|
||||
@@ -35,8 +54,19 @@
|
||||
--color-ac2: #427b58;
|
||||
|
||||
--color-err: #cc241d;
|
||||
|
||||
--color-col1: #9d0006;
|
||||
--color-col2: #79740e;
|
||||
--color-col3: #b57614;
|
||||
--color-col4: #076678;
|
||||
--color-col5: #8f3f71;
|
||||
--color-col6: #427b58;
|
||||
--color-col7: #7c6f65;
|
||||
--color-col8: #af3a03;
|
||||
}
|
||||
|
||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
@@ -59,10 +89,6 @@ body * {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
audio::-webkit-media-controls-panel {
|
||||
background-color: var(--color-bg1);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
:root {
|
||||
font-size: calc((100vw / 1920) * 56);
|
||||
@@ -74,4 +100,4 @@ audio::-webkit-media-controls-panel {
|
||||
:root {
|
||||
font-size: calc((100vw / 1920) * 64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Roboto } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import "./globals.css";
|
||||
|
||||
const roboto = Roboto({ subsets: ["latin"] });
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="ru" suppressHydrationWarning>
|
||||
<body className={roboto.className}>
|
||||
<ThemeProvider enableSystem={false} defaultTheme="light">
|
||||
<ThemeProvider enableSystem={false} defaultTheme="dark">
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ChatWindow from "@/widgets/chat-window";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -6,5 +7,9 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default async function Root() {
|
||||
return <></>;
|
||||
return (
|
||||
<main className="w-full h-full flex items-center justify-center p-8">
|
||||
<ChatWindow />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
5
frontend/src/entities/message/index.ts
Normal file
5
frontend/src/entities/message/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { MessageService } from "./message.service";
|
||||
|
||||
export { type IMessage, messageSchema, messagesSchema } from "./message.schema";
|
||||
|
||||
export default MessageService;
|
||||
11
frontend/src/entities/message/message.schema.ts
Normal file
11
frontend/src/entities/message/message.schema.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { object, string, number, InferType, array } from "yup";
|
||||
|
||||
export const messageSchema = object({
|
||||
id: number().required().positive().integer(),
|
||||
text: string().required(),
|
||||
timeOfSend: string().required(),
|
||||
sender: string().required(),
|
||||
});
|
||||
export const messagesSchema = array(messageSchema);
|
||||
|
||||
export type IMessage = InferType<typeof messageSchema>;
|
||||
16
frontend/src/entities/message/message.service.ts
Normal file
16
frontend/src/entities/message/message.service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import HTTPService from "@/shared/services/http";
|
||||
import { messageSchema, messagesSchema } from "./message.schema";
|
||||
|
||||
export abstract class MessageService {
|
||||
public static getTopOfHistory = () =>
|
||||
HTTPService.get(`message`, messagesSchema);
|
||||
|
||||
public static sendMessage = (message: string, senderUUID: string) =>
|
||||
HTTPService.post(`message`, messageSchema, {
|
||||
body: {
|
||||
text: message,
|
||||
timeOfSend: new Date().toISOString(),
|
||||
sender: senderUUID,
|
||||
},
|
||||
});
|
||||
}
|
||||
5
frontend/src/entities/user/index.ts
Normal file
5
frontend/src/entities/user/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { UserService } from "./user.service";
|
||||
|
||||
export { type IUser, userSchema } from "./user.schema";
|
||||
|
||||
export default UserService;
|
||||
8
frontend/src/entities/user/user.schema.ts
Normal file
8
frontend/src/entities/user/user.schema.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { object, string, number, InferType } from "yup";
|
||||
|
||||
export const userSchema = object({
|
||||
uuid: string().uuid().required(),
|
||||
color: number().min(1).max(8).required(),
|
||||
});
|
||||
|
||||
export type IUser = InferType<typeof userSchema>;
|
||||
25
frontend/src/entities/user/user.service.ts
Normal file
25
frontend/src/entities/user/user.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { v1 as uuidV1 } from "uuid";
|
||||
import { IUser } from "./user.schema";
|
||||
|
||||
export abstract class UserService {
|
||||
private static pickedColors: IUser["color"][] = [];
|
||||
|
||||
public static generateMe = (): IUser => ({
|
||||
uuid: uuidV1(),
|
||||
color: 8,
|
||||
});
|
||||
|
||||
public static generateUser = (uuid: IUser["uuid"]): IUser => {
|
||||
const newColor = Array.from(Array(7).keys())
|
||||
.map((i) => i + 1)
|
||||
.reduce(
|
||||
(p, c) => (!p && !this.pickedColors.includes(c) ? c : p),
|
||||
undefined as number | undefined
|
||||
);
|
||||
this.pickedColors.push(newColor ?? 1);
|
||||
return {
|
||||
uuid: uuid,
|
||||
color: newColor ?? 1,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { SunIcon } from "@/shared/assets/icons";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
export const ColorSchemeSwitch = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SunIcon
|
||||
className="h-10 min-w-10 max-w-10 cursor-pointer bg-bg3 p-1 rounded-lg hover:bg-bg4 transition"
|
||||
onClick={() => setTheme(theme == "light" ? "dark" : "light")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
3
frontend/src/features/colorSchemeSwitch/index.ts
Normal file
3
frontend/src/features/colorSchemeSwitch/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { ColorSchemeSwitch } from "./colorSchemeSwitch";
|
||||
|
||||
export default ColorSchemeSwitch;
|
||||
3
frontend/src/features/messagesList/index.ts
Normal file
3
frontend/src/features/messagesList/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { MessagesList } from "./messagesList";
|
||||
|
||||
export default MessagesList;
|
||||
28
frontend/src/features/messagesList/message.tsx
Normal file
28
frontend/src/features/messagesList/message.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { IMessage } from "@/entities/message";
|
||||
import { IUser } from "@/entities/user";
|
||||
|
||||
export const Message = ({
|
||||
message,
|
||||
color,
|
||||
align = "right",
|
||||
}: {
|
||||
message: IMessage;
|
||||
color: IUser["color"] | undefined;
|
||||
align?: "left" | "right";
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
style={{
|
||||
background: `var(--color-col${color})`,
|
||||
borderRadius: `var(--radius-xl)`,
|
||||
[align === "right"
|
||||
? "borderBottomRightRadius"
|
||||
: "borderBottomLeftRadius"]: 0,
|
||||
}}
|
||||
className={`max-w-20 p-2`}
|
||||
>
|
||||
{message.text}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
73
frontend/src/features/messagesList/messagesList.tsx
Normal file
73
frontend/src/features/messagesList/messagesList.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import MessageService from "@/entities/message";
|
||||
import UserService, { IUser } from "@/entities/user";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { Message } from "./message";
|
||||
|
||||
export const MessagesList = () => {
|
||||
const { data: users, mutate: mutateUsers } = useSWR<IUser[]>("users");
|
||||
const { data: messages, mutate: mutateMessages } = useSWR(
|
||||
"messages",
|
||||
MessageService.getTopOfHistory,
|
||||
{ revalidateOnFocus: false }
|
||||
);
|
||||
const { data: me } = useSWR<IUser>("me");
|
||||
const [websocket, updateWebSocket] = useState<WebSocket>();
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const getUserColor = (userUUID: IUser["uuid"]) =>
|
||||
users?.find((user) => user.uuid === userUUID)?.color;
|
||||
|
||||
useEffect(() => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
const newUsers: IUser[] = [];
|
||||
messages?.forEach((message) => {
|
||||
if (
|
||||
!users?.concat(newUsers).find((user) => user.uuid === message.sender)
|
||||
) {
|
||||
const newUser = UserService.generateUser(message.sender);
|
||||
newUsers.push(newUser);
|
||||
}
|
||||
});
|
||||
mutateUsers([...(users ?? []), ...newUsers]);
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!websocket && messages && process.env.NEXT_PUBLIC_WS_URL) {
|
||||
const websocket = new WebSocket(process.env.NEXT_PUBLIC_WS_URL);
|
||||
websocket.onmessage = (event) => {
|
||||
mutateMessages([...(messages ?? []), JSON.parse(event.data)], {
|
||||
revalidate: false,
|
||||
});
|
||||
};
|
||||
websocket.onclose = function () {
|
||||
setTimeout(() => updateWebSocket(undefined), 1000);
|
||||
};
|
||||
updateWebSocket(websocket);
|
||||
}
|
||||
return () => websocket?.close();
|
||||
}, [websocket, messages]);
|
||||
|
||||
return (
|
||||
<div className="max-w-full h-full overflow-auto flex flex-col gap-2">
|
||||
{messages?.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className="w-full flex"
|
||||
style={{
|
||||
justifyContent: message.sender === me?.uuid ? "end" : "start",
|
||||
}}
|
||||
>
|
||||
<Message
|
||||
message={message}
|
||||
color={getUserColor(message.sender)}
|
||||
align={message.sender === me?.uuid ? "right" : "left"}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div ref={bottomRef}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
3
frontend/src/features/sendMessage/index.ts
Normal file
3
frontend/src/features/sendMessage/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { SendMessage } from "./sendMessage";
|
||||
|
||||
export default SendMessage;
|
||||
41
frontend/src/features/sendMessage/sendMessage.tsx
Normal file
41
frontend/src/features/sendMessage/sendMessage.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import MessageService from "@/entities/message";
|
||||
import UserService from "@/entities/user";
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
export const SendMessage = () => {
|
||||
const [message, updateMessage] = useState<string>("");
|
||||
const { data: me } = useSWR(`me`, UserService.generateMe);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex items-center gap-4">
|
||||
<input
|
||||
type="text"
|
||||
className={
|
||||
"w-full h-full px-4 py-1 rounded-lg" +
|
||||
" focus:outline-1 outline-fg4 focus:shadow-md"
|
||||
}
|
||||
placeholder="Напиши сообщение..."
|
||||
value={message}
|
||||
onChange={(e) => updateMessage(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className={
|
||||
"h-full px-4 rounded-lg bg-ac2 disabled:bg-bg3" +
|
||||
" cursor-pointer hover:shadow-md text-fg0 disabled:text-fg4"
|
||||
}
|
||||
disabled={!message || !me}
|
||||
onClick={() => {
|
||||
if (!!me) {
|
||||
MessageService.sendMessage(message, me.uuid);
|
||||
updateMessage("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Отправить
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
2
frontend/src/shared/assets/icons/index.ts
Normal file
2
frontend/src/shared/assets/icons/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { SunIcon } from "./sunIcon";
|
||||
export { SendIcon } from "./sendIcon";
|
||||
33
frontend/src/shared/assets/icons/sendIcon.tsx
Normal file
33
frontend/src/shared/assets/icons/sendIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
export const SendIcon = ({
|
||||
className,
|
||||
onClick,
|
||||
}: {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g
|
||||
id="SVGRepo_tracerCarrier"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
{" "}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M1.265 4.42619C1.04293 2.87167 2.6169 1.67931 4.05323 2.31397L21.8341 10.1706C23.423 10.8727 23.423 13.1273 21.8341 13.8294L4.05323 21.686C2.6169 22.3207 1.04293 21.1283 1.265 19.5738L1.99102 14.4917C2.06002 14.0087 2.41458 13.6156 2.88791 13.4972L8.87688 12L2.88791 10.5028C2.41458 10.3844 2.06002 9.99129 1.99102 9.50829L1.265 4.42619ZM21.0257 12L3.2449 4.14335L3.89484 8.69294L12.8545 10.9328C13.9654 11.2106 13.9654 12.7894 12.8545 13.0672L3.89484 15.3071L3.2449 19.8566L21.0257 12Z"
|
||||
fill="var(--color-fg0)"
|
||||
></path>{" "}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
31
frontend/src/shared/assets/icons/sunIcon.tsx
Normal file
31
frontend/src/shared/assets/icons/sunIcon.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
export const SunIcon = ({
|
||||
className,
|
||||
onClick,
|
||||
}: {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
d="M12 3V4M12 20V21M4 12H3M6.31412 6.31412L5.5 5.5M17.6859
|
||||
6.31412L18.5 5.5M6.31412 17.69L5.5 18.5001M17.6859
|
||||
17.69L18.5 18.5001M21 12H20M16 12C16 14.2091 14.2091
|
||||
16 12 16C9.79086 16 8 14.2091 8 12C8 9.79086 9.79086
|
||||
8 12 8C14.2091 8 16 9.79086 16 12Z"
|
||||
stroke="var(--color-fg1)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
95
frontend/src/shared/services/http/http.ts
Normal file
95
frontend/src/shared/services/http/http.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { AnySchema } from "yup";
|
||||
|
||||
export type RequestCacheOptions = {
|
||||
cache?: RequestCache;
|
||||
next?: NextFetchRequestConfig;
|
||||
};
|
||||
|
||||
type GetRequestOptions = RequestCacheOptions & {
|
||||
headers?: HeadersInit;
|
||||
};
|
||||
|
||||
type RequestOptions = GetRequestOptions & {
|
||||
body?: BodyInit | object;
|
||||
stringify?: boolean;
|
||||
};
|
||||
|
||||
export abstract class HTTPService {
|
||||
private static deepUndefinedToNull(o?: object): object | undefined {
|
||||
if (Array.isArray(o)) return o;
|
||||
if (o)
|
||||
return Object.fromEntries(
|
||||
Object.entries(o).map(([k, v]) => {
|
||||
if (v === undefined) return [k, null];
|
||||
if (typeof v === "object") return [k, this.deepUndefinedToNull(v)];
|
||||
return [k, v];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public static async request<Y extends AnySchema>(
|
||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||
url: string,
|
||||
schema: Y,
|
||||
options?: RequestOptions
|
||||
) {
|
||||
return await fetch(
|
||||
`${process.env.NEXT_PUBLIC_BASE_PROTOCOL}://` +
|
||||
`${process.env.NEXT_PUBLIC_BASE_DOMAIN}:` +
|
||||
`${process.env.NEXT_PUBLIC_BASE_PORT}` +
|
||||
`${process.env.NEXT_PUBLIC_API_PATTERN}${url}`,
|
||||
{
|
||||
method: method,
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
...((options?.stringify ?? true) != true
|
||||
? {}
|
||||
: { "Content-Type": "application/json" }),
|
||||
...options?.headers,
|
||||
},
|
||||
body:
|
||||
(options?.stringify ?? true) != true
|
||||
? (options?.body as BodyInit)
|
||||
: JSON.stringify(
|
||||
this.deepUndefinedToNull(options?.body as object | undefined)
|
||||
),
|
||||
cache: options?.cache ?? options?.next ? undefined : "no-cache",
|
||||
next: options?.next ?? {},
|
||||
}
|
||||
)
|
||||
.then((r) => {
|
||||
if (r && r.ok) return r;
|
||||
else throw Error("Response ok = false");
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then(async (d) => await schema.validate(d))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static async get<Y extends AnySchema>(
|
||||
url: string,
|
||||
schema: Y,
|
||||
options?: GetRequestOptions
|
||||
) {
|
||||
return await this.request<Y>("GET", url, schema, options);
|
||||
}
|
||||
|
||||
public static async post<Y extends AnySchema>(
|
||||
url: string,
|
||||
schema: Y,
|
||||
options?: RequestOptions
|
||||
) {
|
||||
return await this.request<Y>("POST", url, schema, options);
|
||||
}
|
||||
|
||||
public static async put<Y extends AnySchema>(
|
||||
url: string,
|
||||
schema: Y,
|
||||
options?: RequestOptions
|
||||
) {
|
||||
return await this.request<Y>("PUT", url, schema, options);
|
||||
}
|
||||
}
|
||||
3
frontend/src/shared/services/http/index.ts
Normal file
3
frontend/src/shared/services/http/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { HTTPService } from "./http";
|
||||
|
||||
export default HTTPService;
|
||||
3
frontend/src/widgets/chat-window/index.ts
Normal file
3
frontend/src/widgets/chat-window/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { ChatWindow } from "./window";
|
||||
|
||||
export default ChatWindow;
|
||||
25
frontend/src/widgets/chat-window/window.tsx
Normal file
25
frontend/src/widgets/chat-window/window.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import ColorSchemeSwitch from "@/features/colorSchemeSwitch";
|
||||
import MessagesList from "@/features/messagesList";
|
||||
import SendMessage from "@/features/sendMessage";
|
||||
|
||||
export const ChatWindow = () => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-lg h-full md:size-120 bg-bg1 p-4 rounded-lg " +
|
||||
"drop-shadow-2xl flex flex-col gap-4 justify-between"
|
||||
}
|
||||
>
|
||||
<MessagesList />
|
||||
<div
|
||||
className={
|
||||
"w-full size-14 bg-bg2 p-2 rounded-lg drop-shadow-md " +
|
||||
"flex flex-row gap-4 items-center justify-between"
|
||||
}
|
||||
>
|
||||
<ColorSchemeSwitch />
|
||||
<SendMessage />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user