diff --git a/i18n.server.js b/i18n.server.js index 27c10e0..1ffdefe 100644 --- a/i18n.server.js +++ b/i18n.server.js @@ -16,7 +16,7 @@ export function createI18nInstance() { .init({ fallbackLng: "en", preload: ["en", "ru"], - ns: ["common", "auth"], + ns: ["common", "auth", "chat"], defaultNS: "common", backend: { loadPath: __dirname + "/locales/{{lng}}/{{ns}}.json", diff --git a/index.html b/index.html index 0c5dc1b..8246259 100644 --- a/index.html +++ b/index.html @@ -11,6 +11,6 @@
- + diff --git a/locales/ru/chat.json b/locales/ru/chat.json new file mode 100644 index 0000000..ce4ca34 --- /dev/null +++ b/locales/ru/chat.json @@ -0,0 +1,3 @@ +{ + "title": "Чаты" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 592852a..be55bc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,12 @@ "dependencies": { "@reduxjs/toolkit": "^2.5.1", "antd": "^5.23.0", + "classnames": "^2.5.1", "compression": "^1.7.5", "cookie-parser": "^1.4.7", "cross-env": "^7.0.3", "express": "^4.21.2", + "express-http-proxy": "^2.1.1", "i18next": "^24.2.2", "i18next-browser-languagedetector": "^8.0.2", "i18next-fs-backend": "^2.6.0", @@ -3191,6 +3193,35 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-http-proxy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/express-http-proxy/-/express-http-proxy-2.1.1.tgz", + "integrity": "sha512-4aRQRqDQU7qNPV5av0/hLcyc0guB9UP71nCYrQEYml7YphTo8tmWf3nDZWdTJMMjFikyz9xKXaURor7Chygdwg==", + "license": "MIT", + "dependencies": { + "debug": "^3.0.1", + "es6-promise": "^4.1.1", + "raw-body": "^2.3.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/express-http-proxy/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/express-http-proxy/node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, "node_modules/express/node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", diff --git a/package.json b/package.json index cbecb67..397b4e8 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,12 @@ "dependencies": { "@reduxjs/toolkit": "^2.5.1", "antd": "^5.23.0", + "classnames": "^2.5.1", "compression": "^1.7.5", "cookie-parser": "^1.4.7", "cross-env": "^7.0.3", "express": "^4.21.2", + "express-http-proxy": "^2.1.1", "i18next": "^24.2.2", "i18next-browser-languagedetector": "^8.0.2", "i18next-fs-backend": "^2.6.0", diff --git a/server.js b/server.js index 137dd41..4fa37ce 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,7 @@ import express from "express"; import { createI18nInstance } from "./i18n.server.js"; import i18nextMiddleware from "i18next-http-middleware"; import cookieParser from "cookie-parser"; +import proxy from "express-http-proxy"; // Constants const isProduction = process.env.NODE_ENV === "production"; @@ -18,6 +19,22 @@ const templateHtml = isProduction const app = express(); app.use(cookieParser()); +app.use( + "/api", + proxy("https://194.67.125.199:8443/", { + userResHeaderDecorator(headers, userReq, userRes, proxyReq, proxyRes) { + if ( + (userReq.url === "/auth" || userReq.url === "/refresh") && + userRes.statusCode === 200 + ) { + headers["set-cookie"][0] += "; path=/"; + } + // recieves an Object of headers, returns an Object of headers. + return headers; + }, + }), +); + // Add Vite or respective production middlewares /** @type {import('vite').ViteDevServer | undefined} */ let vite; @@ -46,8 +63,6 @@ app.use((req, res, next) => { // Serve HTML app.use("*", async (req, res) => { - console.log(req.cookies); - try { const url = req.originalUrl.replace(base, "/"); @@ -70,7 +85,11 @@ app.use("*", async (req, res) => { render = (await import("./dist/server/entry-server.js")).render; } - const rendered = await render(url, req.i18n); + const rendered = await render(url, req.i18n, req.cookies); + + if (rendered.returnCookie) { + res.set("Set-Cookie", `${rendered.returnCookie};path=/`); + } const html = template .replace( diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index af575c1..b15ab57 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -4,6 +4,7 @@ import { HeaderComponent } from "./components/Header"; import { Auth } from "./pages/auth"; import { Favourites } from "./pages/favourites/Favourites.tsx"; import { CreateBook } from "./pages/createBook"; +import { Chat } from "./pages/chat/Chat.tsx"; const Router = import.meta.env.SSR ? StaticRouter : BrowserRouter; @@ -17,6 +18,7 @@ export function AppRouter({ location }: { location: string }) { } /> } /> } /> + } /> 404 page not found} /> diff --git a/src/antdConfig.ts b/src/antdConfig.ts index 5ba9952..69dce75 100644 --- a/src/antdConfig.ts +++ b/src/antdConfig.ts @@ -16,6 +16,9 @@ export const antdThemeConfig: ThemeConfig = { controlHeight: 40, paddingContentHorizontal: 25, }, + List: { + itemPadding: "12px", + }, Input: { colorText: "#000000", activeBorderColor: "rgba(42, 127, 255, 80)", diff --git a/src/entry-server.tsx b/src/entry-server.tsx index 8be04cd..84e9194 100644 --- a/src/entry-server.tsx +++ b/src/entry-server.tsx @@ -5,12 +5,27 @@ import type Entity from "@ant-design/cssinjs/es/Cache"; import type { i18n } from "i18next"; import { setupStore } from "./store.ts"; import { sharebookApi } from "./services/api/sharebookApi.ts"; - -export async function render(location: string, i18n: i18n) { +import { + setAccessToken, + setRefreshCookie, + setReturnCookie, +} from "./services/auth/authSlice.tsx"; + +export async function render( + location: string, + i18n: i18n, + cookie: Record, +) { const cache: Entity = createCache(); const store = setupStore(); + const cookieString = Object.entries(cookie) + .map(([key, value]) => `${key}=${value}`) + .join(";"); + + store.dispatch(setRefreshCookie(cookieString)); + const element = ( ); @@ -19,11 +34,17 @@ export async function render(location: string, i18n: i18n) { await Promise.all(store.dispatch(sharebookApi.util.getRunningQueriesThunk())); + const returnCookie = store.getState().auth?.returnCookie; + + store.dispatch(setRefreshCookie(null)); + store.dispatch(setReturnCookie(null)); + store.dispatch(setAccessToken(null)); + const body = renderToString(element); const head = extractStyle(cache); const state = store.getState(); - return { body, head, state }; + return { body, head, state, returnCookie }; } diff --git a/src/pages/chat/Chat.tsx b/src/pages/chat/Chat.tsx new file mode 100644 index 0000000..5adb404 --- /dev/null +++ b/src/pages/chat/Chat.tsx @@ -0,0 +1,18 @@ +import styles from "./chat.module.scss"; +import { ChatUserList } from "./userList"; +import { ChatCurrentUser } from "./currentUser"; +import { ChatInput } from "./input"; +import { ChatBody } from "./body"; + +export function Chat() { + return ( +
+ +
+ + + +
+
+ ); +} diff --git a/src/pages/chat/body/ChatBody.tsx b/src/pages/chat/body/ChatBody.tsx new file mode 100644 index 0000000..b99c7c4 --- /dev/null +++ b/src/pages/chat/body/ChatBody.tsx @@ -0,0 +1,24 @@ +import styles from "./chatBody.module.scss"; +import { useAppSelector } from "../../../store.ts"; +import { useGetCorrespondenceQuery } from "../../../services/api/sharebookApi.ts"; +import { ChatMessage } from "../chatMessage"; + +export function ChatBody() { + const activeChatId = useAppSelector((state) => state.chat.activeId); + + const response = useGetCorrespondenceQuery({ + firstUserId: "6", + secondUserId: "4", + zone: 4, + }); + + if (!activeChatId) return
Кому писать?
; + + return ( +
+ {response.currentData?.map((item) => ( + + ))} +
+ ); +} diff --git a/src/pages/chat/body/chatBody.module.scss b/src/pages/chat/body/chatBody.module.scss new file mode 100644 index 0000000..d1e43f1 --- /dev/null +++ b/src/pages/chat/body/chatBody.module.scss @@ -0,0 +1,23 @@ +.wrapper { + flex: 1; + display: flex; + flex-direction: column; +} + +.blank { + flex: 1; +} + +.centered { + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.scroll { + flex: 1; + display: flex; + flex-direction: column; + justify-content: flex-end; +} \ No newline at end of file diff --git a/src/pages/chat/body/index.ts b/src/pages/chat/body/index.ts new file mode 100644 index 0000000..a2b881d --- /dev/null +++ b/src/pages/chat/body/index.ts @@ -0,0 +1 @@ +export { ChatBody } from "./ChatBody.tsx"; diff --git a/src/pages/chat/chat.module.scss b/src/pages/chat/chat.module.scss new file mode 100644 index 0000000..589a353 --- /dev/null +++ b/src/pages/chat/chat.module.scss @@ -0,0 +1,22 @@ +.container { + max-width: 1400px; + margin: 0 auto; + height: calc(100vh - 66px); + display: flex; +} + +.chatBodyWrapper { + flex: 1; + display: flex; + flex-direction: column; + align-items: stretch; + position: relative; +} + +.dropdownOverlay { + backdrop-filter: blur(50px); + + ul { + margin-top: 2px !important; + } +} \ No newline at end of file diff --git a/src/pages/chat/chatMessage/ChatMessage.tsx b/src/pages/chat/chatMessage/ChatMessage.tsx new file mode 100644 index 0000000..f9c3f17 --- /dev/null +++ b/src/pages/chat/chatMessage/ChatMessage.tsx @@ -0,0 +1,19 @@ +import styles from "./chatMessage.module.scss"; +import { MessageDto } from "../../../services/api/sharebookApi.ts"; +import cn from "classnames"; + +interface ChatMessageProps { + message: MessageDto; + profileId: string | number; +} + +export function ChatMessage(props: ChatMessageProps) { + return ( +
+ {props.message.text} + + {props.message.departureDate?.split("T")[1].slice(0, 5)} + +
+ ); +} diff --git a/src/pages/chat/chatMessage/chatMessage.module.scss b/src/pages/chat/chatMessage/chatMessage.module.scss new file mode 100644 index 0000000..4044c98 --- /dev/null +++ b/src/pages/chat/chatMessage/chatMessage.module.scss @@ -0,0 +1,23 @@ +.wrapper { + border-radius: 12px; + background: #F1F4F6; + padding: 10px 40px 10px 15px; + margin-bottom: 16px; + max-width: 530px; + width: fit-content; + position: relative; + + .time { + position: absolute; + right: 6px; + bottom: 6px; + font-size: 12px; + line-height: 15px; + } + + &.own { + margin-left: auto; + background: var(--ant-color-primary); + color: #fff; + } +} \ No newline at end of file diff --git a/src/pages/chat/chatMessage/index.ts b/src/pages/chat/chatMessage/index.ts new file mode 100644 index 0000000..219e21a --- /dev/null +++ b/src/pages/chat/chatMessage/index.ts @@ -0,0 +1 @@ +export { ChatMessage } from "./ChatMessage.tsx"; diff --git a/src/pages/chat/chatSlice.ts b/src/pages/chat/chatSlice.ts new file mode 100644 index 0000000..0a2469c --- /dev/null +++ b/src/pages/chat/chatSlice.ts @@ -0,0 +1,23 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +interface ChatState { + activeId: string | null; +} + +const initialState: ChatState = { + activeId: null, +}; + +const chatSlice = createSlice({ + name: "chat", + initialState, + reducers: { + setActiveId(state, action: PayloadAction) { + state.activeId = action.payload; + }, + }, +}); + +export const { setActiveId } = chatSlice.actions; + +export const chatReducer = chatSlice.reducer; diff --git a/src/pages/chat/currentUser/ChatCurrentUser.tsx b/src/pages/chat/currentUser/ChatCurrentUser.tsx new file mode 100644 index 0000000..e52458a --- /dev/null +++ b/src/pages/chat/currentUser/ChatCurrentUser.tsx @@ -0,0 +1,67 @@ +import users from "../users.json"; +import { useAppSelector } from "../../../store.ts"; +import defaultAvatar from "../defaultChatAvatar.png"; +import styles from "./chatCurrentUser.module.scss"; +import { SvgMenu } from "./SvgMenu.tsx"; +import { Dropdown } from "antd"; +import { SvgTrash } from "./SvgTrash.tsx"; + +const actions = [ + { + danger: true, + label: ( +
+ + Удалить чат +
+ ), + key: "delete", + }, +]; + +export function ChatCurrentUser() { + // const users = useAppSelector(sharebookApi.endpoints.getUsers.select); + const activeChatId = useAppSelector((state) => state.chat.activeId); + const activeUser = users.find((user) => user.author.id === activeChatId); + + if (!activeUser) { + return null; + } + + return ( +
+ +
+
{activeUser.author.name}
+
+ Книга{" "} + + «{activeUser.book.title}» + +
+
+
+ + + +
+ ); +} diff --git a/src/pages/chat/currentUser/SvgMenu.tsx b/src/pages/chat/currentUser/SvgMenu.tsx new file mode 100644 index 0000000..6b96dde --- /dev/null +++ b/src/pages/chat/currentUser/SvgMenu.tsx @@ -0,0 +1,15 @@ +export function SvgMenu() { + return ( + + + + + + ); +} diff --git a/src/pages/chat/currentUser/SvgTrash.tsx b/src/pages/chat/currentUser/SvgTrash.tsx new file mode 100644 index 0000000..00506eb --- /dev/null +++ b/src/pages/chat/currentUser/SvgTrash.tsx @@ -0,0 +1,24 @@ +export function SvgTrash() { + return ( + + + + + ); +} diff --git a/src/pages/chat/currentUser/chatCurrentUser.module.scss b/src/pages/chat/currentUser/chatCurrentUser.module.scss new file mode 100644 index 0000000..458fd61 --- /dev/null +++ b/src/pages/chat/currentUser/chatCurrentUser.module.scss @@ -0,0 +1,73 @@ +.wrapper { + display: flex; + padding: 15px; + align-items: center; + background: rgba(255, 255, 255, 0.6); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + backdrop-filter: blur(30px); + position: absolute; + top: 0; + left: 0; + right: 0; +} + +.blank { + flex: 1; +} + +.avatar { + border-radius: 50%; + margin-right: 12px; + width: 45px; + height: 45px; + flex-shrink: 0; + object-fit: cover; +} + +.textContent { + display: flex; + flex-direction: column; + justify-content: space-around; +} + +.authorName { + font-size: 16px; + font-weight: 500; + line-height: 20px; +} + +.bookTitle { + color: #909090; + font-size: 14px; + line-height: 18px; + text-overflow: ellipsis; + overflow-y: hidden; +} + +.dropdownButton { + line-height: 0; + border: none; + box-shadow: none; + background: none; + cursor: pointer; + padding: 12px; + margin: 0 24px; +} + +.delete { + display: flex; + align-items: center; + padding: 6px 10px; + margin: -6px -10px; + + svg { + margin-right: 10px; + stroke: #FF2A2A; + } + + &:hover { + svg { + stroke: #fff; + } + } +} \ No newline at end of file diff --git a/src/pages/chat/currentUser/index.ts b/src/pages/chat/currentUser/index.ts new file mode 100644 index 0000000..6e3620c --- /dev/null +++ b/src/pages/chat/currentUser/index.ts @@ -0,0 +1 @@ +export { ChatCurrentUser } from "./ChatCurrentUser.tsx"; diff --git a/src/pages/chat/defaultChatAvatar.png b/src/pages/chat/defaultChatAvatar.png new file mode 100644 index 0000000..c278736 Binary files /dev/null and b/src/pages/chat/defaultChatAvatar.png differ diff --git a/src/pages/chat/index.ts b/src/pages/chat/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/chat/input/ChatInput.tsx b/src/pages/chat/input/ChatInput.tsx new file mode 100644 index 0000000..f830bfd --- /dev/null +++ b/src/pages/chat/input/ChatInput.tsx @@ -0,0 +1,32 @@ +import { Input } from "antd"; +import styles from "./chatInput.module.scss"; +import type { KeyboardEventHandler } from "react"; +import { useAppSelector } from "../../../store.ts"; + +const { TextArea } = Input; + +export function ChatInput() { + const activeChatId = useAppSelector((state) => state.chat.activeId); + + const onPressEnter: KeyboardEventHandler = (event) => { + if (event.shiftKey) { + console.log("do nothing"); + + return; + } + + console.log((event.target as HTMLTextAreaElement).value); + }; + + if (!activeChatId) return null; + + return ( +
+