From 0e66b525d929ee790ab3ee920d770a597c65d03f Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Wed, 26 Mar 2025 15:33:56 +0300 Subject: [PATCH 01/78] fix icons centering in user card --- .../src/components/chatMember.card.component.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/src/components/chatMember.card.component.tsx b/client/src/components/chatMember.card.component.tsx index c39e953..edbdd5f 100644 --- a/client/src/components/chatMember.card.component.tsx +++ b/client/src/components/chatMember.card.component.tsx @@ -63,12 +63,12 @@ const ChatMemberCardComponent = ( {props.userData.firstName} {props.userData.lastName}

-

- +

+ {props.userData.company}

-

- +

+ {props.userData.role}

@@ -105,12 +105,12 @@ const ChatMemberCardComponent = ( {props.userData.firstName} {props.userData.lastName}

-

- +

+ {props.userData.company}

-

- +

+ {props.userData.role}

From e0440dd5325170c0105531d908601c85d45cc6ab Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 3 Apr 2025 15:53:34 +0300 Subject: [PATCH 02/78] fixed: check chat membership --- .../src/components/requireMembership.component.tsx | 11 ++++++++++- client/src/stores/invite.store.ts | 13 +++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 client/src/stores/invite.store.ts diff --git a/client/src/components/requireMembership.component.tsx b/client/src/components/requireMembership.component.tsx index 556c615..3cbf663 100644 --- a/client/src/components/requireMembership.component.tsx +++ b/client/src/components/requireMembership.component.tsx @@ -2,6 +2,7 @@ import { initData, initDataStartParam } from "@telegram-apps/sdk-react"; import { getChatPreview } from "../api/api"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; +import useInviteStore from "../stores/invite.store"; interface RequireMembershipComponentProps { children: React.ReactNode; @@ -10,12 +11,20 @@ interface RequireMembershipComponentProps { const RequireMembershipComponent = (props: RequireMembershipComponentProps) => { const navigate = useNavigate(); const initDataRaw = initData.raw(); + const inviteStore = useInviteStore(); let chatID = props.chatID !== undefined ? props.chatID : initDataStartParam(); const [isLoading, setIsLoading] = useState(true); const [isMember, setIsMember] = useState(false); const checkMembership = async () => { - if (initDataStartParam() !== undefined) chatID = initDataStartParam(); + if (initDataStartParam() !== undefined) { + if (inviteStore.showedOnce) { + chatID = ""; + } else { + chatID = initDataStartParam(); + inviteStore.setShowedOnce(); + } + } const data = await getChatPreview(initDataRaw ?? "", chatID || ""); if (data !== null && data?.isMember !== null) { setIsMember(data.isMember); diff --git a/client/src/stores/invite.store.ts b/client/src/stores/invite.store.ts new file mode 100644 index 0000000..27dcb3a --- /dev/null +++ b/client/src/stores/invite.store.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +interface InviteStoreState { + showedOnce: boolean; + setShowedOnce: () => void; +} + +const useInviteStore = create((set) => ({ + showedOnce: false, + setShowedOnce: () => set({ showedOnce: true }), +})); + +export default useInviteStore; From 9a3c0ddf6ebfd4b79f98b3fe53b1eae3523cb782 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 3 Apr 2025 16:11:07 +0300 Subject: [PATCH 03/78] fixed: chat membership check x2 --- client/public/config.js | 2 +- .../src/components/requireMembership.component.tsx | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/client/public/config.js b/client/public/config.js index 6aff618..0832de4 100644 --- a/client/public/config.js +++ b/client/public/config.js @@ -1,4 +1,4 @@ window.api = { BOT_USERNAME: "vaniog_tglink_bot", - API_URL: "https://tunnel.leenky.ru", + API_URL: "https://app.dev.leenky.ru", }; diff --git a/client/src/components/requireMembership.component.tsx b/client/src/components/requireMembership.component.tsx index 3cbf663..829865b 100644 --- a/client/src/components/requireMembership.component.tsx +++ b/client/src/components/requireMembership.component.tsx @@ -10,20 +10,18 @@ interface RequireMembershipComponentProps { } const RequireMembershipComponent = (props: RequireMembershipComponentProps) => { const navigate = useNavigate(); - const initDataRaw = initData.raw(); + const inviteStore = useInviteStore(); + + const initDataRaw = initData.raw(); let chatID = props.chatID !== undefined ? props.chatID : initDataStartParam(); const [isLoading, setIsLoading] = useState(true); const [isMember, setIsMember] = useState(false); const checkMembership = async () => { - if (initDataStartParam() !== undefined) { - if (inviteStore.showedOnce) { - chatID = ""; - } else { - chatID = initDataStartParam(); - inviteStore.setShowedOnce(); - } + if (initDataStartParam() !== undefined && !inviteStore.showedOnce) { + chatID = initDataStartParam(); + inviteStore.setShowedOnce(); } const data = await getChatPreview(initDataRaw ?? "", chatID || ""); if (data !== null && data?.isMember !== null) { From 4382569941a9bf44b4508ea02d21594eccb8d7c6 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 3 Apr 2025 17:12:20 +0300 Subject: [PATCH 04/78] feature: scroll memory --- .../components/chatMember.card.component.tsx | 2 +- client/src/pages/chat.page.tsx | 30 +++++++++++++++++-- client/src/stores/chatSearch.store.ts | 4 +++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/client/src/components/chatMember.card.component.tsx b/client/src/components/chatMember.card.component.tsx index edbdd5f..b96cafa 100644 --- a/client/src/components/chatMember.card.component.tsx +++ b/client/src/components/chatMember.card.component.tsx @@ -14,7 +14,7 @@ const cardVariants = { opacity: 1, y: 0, transition: { - delay: index * 0.1, + delay: index > 10 ? index * 0.01 : 11 * 0.01, duration: 0.4, ease: "easeOut", }, diff --git a/client/src/pages/chat.page.tsx b/client/src/pages/chat.page.tsx index f37f7f6..1a10e87 100644 --- a/client/src/pages/chat.page.tsx +++ b/client/src/pages/chat.page.tsx @@ -6,7 +6,7 @@ import ChatPreviewComponent from "../components/chatPreview.component"; import { getChat, getChatPreview, searchInChat } from "../api/api"; import { initData } from "@telegram-apps/sdk-react"; import { ChatData, ChatPreviewData } from "../types/user.interface"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import useChatSearchStore from "../stores/chatSearch.store"; import ChatMemberCardComponent from "../components/chatMember.card.component"; import { motion } from "motion/react"; @@ -21,7 +21,7 @@ const containerVariants = { const ChatPage = () => { const { chatId } = useParams(); const [opened, setOpened] = useState(false); - const { searchQuery, setSearchQuery, chatID, setChatID } = + const { searchQuery, setSearchQuery, chatID, setChatID, setScroll, scroll } = useChatSearchStore(); const [loading, isLoading] = useState(true); @@ -72,6 +72,13 @@ const ChatPage = () => { isLoading(false); }; + const scrollContainerRef = useRef(null); + const handleScroll = () => { + if (scrollContainerRef.current) { + setScroll(scrollContainerRef.current.scrollTop); + } + }; + useEffect(() => { if (chatId !== chatID) { setChatID(chatId ?? ""); @@ -101,10 +108,27 @@ const ChatPage = () => { setOpened(false); }, []); + useEffect(() => { + const timer = setTimeout(() => { + if (scrollContainerRef.current) { + console.log(scroll); + scrollContainerRef.current.scrollTo({ + top: scroll, + behavior: "smooth", + }); + } + }, 100); + + return () => clearTimeout(timer); + }, [loading]); return ( -
+
{previewChatData.isMember === null && (
  • void; setChatID: (chatId: string) => void; + setScroll: (scroll: number) => void; } const useChatSearchStore = create((set) => ({ searchQuery: "", chatID: "", + scroll: 0, + setScroll: (scroll: number) => set(() => ({ scroll: scroll })), setSearchQuery: (query: string) => set(() => ({ searchQuery: query })), setChatID: (chatID: string) => set(() => ({ chatID: chatID })), })); From 0aaec1181c49d24d4c9b5d3bd1e85ccc21d73869 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 3 Apr 2025 17:18:48 +0300 Subject: [PATCH 05/78] scroll memory for chats --- .../src/components/chatPreview.component.tsx | 2 +- client/src/pages/chats.page.tsx | 31 +++++++++++++++++-- client/src/stores/chatsSearch.store.ts | 4 +++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/client/src/components/chatPreview.component.tsx b/client/src/components/chatPreview.component.tsx index 7b6c5dd..c5cd5f6 100644 --- a/client/src/components/chatPreview.component.tsx +++ b/client/src/components/chatPreview.component.tsx @@ -18,7 +18,7 @@ const chatPreviewVariants = { opacity: 1, y: 0, transition: { - delay: index * 0.1, + delay: index > 10 ? index * 0.01 : 11 * 0.01, duration: 0.4, ease: "easeOut", }, diff --git a/client/src/pages/chats.page.tsx b/client/src/pages/chats.page.tsx index 843f121..c23de3b 100644 --- a/client/src/pages/chats.page.tsx +++ b/client/src/pages/chats.page.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useRef } from "react"; import ProfileComponent from "../components/profile.component"; import SearchBarComponent from "../components/searchBar.component"; import { ChatPreviewData } from "../types/user.interface"; @@ -24,7 +24,8 @@ const ChatsPage = () => { const [opened, setOpened] = useState(false); const [isLoading, setIsLoading] = useState(true); const [chats, setChats] = useState([]); - const { searchQuery, setSearchQuery } = useChatsSearchStore(); + const { searchQuery, setSearchQuery, scroll, setScroll } = + useChatsSearchStore(); const userStore = useUserStore(); const deleteHandler = async (chatPreviewData: ChatPreviewData) => { popup @@ -79,10 +80,34 @@ const ChatsPage = () => { fetchChats(searchQuery); setOpened(true); }, [searchQuery]); + + const scrollContainerRef = useRef(null); + const handleScroll = () => { + if (scrollContainerRef.current) { + setScroll(scrollContainerRef.current.scrollTop); + } + }; + useEffect(() => { + const timer = setTimeout(() => { + if (scrollContainerRef.current) { + console.log(scroll); + scrollContainerRef.current.scrollTo({ + top: scroll, + behavior: "smooth", + }); + } + }, 100); + + return () => clearTimeout(timer); + }, [isLoading]); return ( <> -
    +

    Чаты

    diff --git a/client/src/stores/chatsSearch.store.ts b/client/src/stores/chatsSearch.store.ts index 81a5c7c..a363277 100644 --- a/client/src/stores/chatsSearch.store.ts +++ b/client/src/stores/chatsSearch.store.ts @@ -2,11 +2,15 @@ import { create } from "zustand"; interface ChatsSearchState { searchQuery: string; + scroll: number; setSearchQuery: (query: string) => void; + setScroll: (scroll: number) => void; } const useChatsSearchStore = create((set) => ({ searchQuery: "", + scroll: 0, + setScroll: (scroll: number) => set(() => ({ scroll: scroll })), setSearchQuery: (query: string) => set(() => ({ searchQuery: query })), })); export default useChatsSearchStore; From 63a77a185054d9f2c30dd4ae74ef053c609ec019 Mon Sep 17 00:00:00 2001 From: Timur Valeev <76071256+PriestFaria@users.noreply.github.com> Date: Thu, 1 May 2025 15:04:49 +0300 Subject: [PATCH 06/78] Refactor/client/query (#51) * api hooks * membership and stores refactor * invitation page refactor * tg launch params store * edit profile refactor * remove scroll logginh * refactor chats page * fixed fucking caching for god's sake * chat page query refactor --- client/package-lock.json | 27 ++++ client/package.json | 1 + client/src/App.tsx | 83 +++++------ client/src/api/api.ts | 45 +++--- client/src/components/about.component.tsx | 2 +- .../src/components/profileView.component.tsx | 66 --------- .../components/protectedRoute.component.tsx | 8 +- .../requireMembership.component.tsx | 41 ++---- client/src/components_v2/About.tsx | 42 ++++++ client/src/components_v2/Button.tsx | 45 ++++++ client/src/components_v2/ChatHeader.tsx | 33 +++++ client/src/components_v2/ChatMemberCard.tsx | 33 +++++ client/src/hooks/useChat.ts | 13 ++ client/src/hooks/useChatPreview.ts | 13 ++ client/src/hooks/useChats.ts | 13 ++ client/src/hooks/useCreateMe.ts | 27 ++++ client/src/hooks/useJoinMe.ts | 26 ++++ client/src/hooks/useLeaveChat.ts | 21 +++ client/src/hooks/useMe.ts | 13 ++ client/src/hooks/useMePreview.ts | 13 ++ client/src/hooks/useSearchChats.ts | 15 ++ client/src/hooks/useSearchUsers.ts | 15 ++ client/src/hooks/useUpdateMe.ts | 27 ++++ client/src/hooks/useUser.ts | 13 ++ client/src/main.tsx | 8 +- client/src/pages/chat.page.tsx | 139 ++++-------------- client/src/pages/chats.page.tsx | 90 ++++-------- client/src/pages/currentProfile.page.tsx | 30 +--- client/src/pages/editProfile.page.tsx | 91 ++++++------ client/src/pages/initial.page.tsx | 13 +- client/src/pages/invitation.page.tsx | 39 ++--- client/src/pages/profile.page.tsx | 18 +-- client/src/pages/registration.page.tsx | 90 +++++------- client/src/stores/InitData.store.ts | 41 ++++++ client/src/stores/StartData.store.ts | 8 + client/src/utils/InitDataWrapper.tsx | 16 ++ 36 files changed, 700 insertions(+), 518 deletions(-) delete mode 100644 client/src/components/profileView.component.tsx create mode 100644 client/src/components_v2/About.tsx create mode 100644 client/src/components_v2/Button.tsx create mode 100644 client/src/components_v2/ChatHeader.tsx create mode 100644 client/src/components_v2/ChatMemberCard.tsx create mode 100644 client/src/hooks/useChat.ts create mode 100644 client/src/hooks/useChatPreview.ts create mode 100644 client/src/hooks/useChats.ts create mode 100644 client/src/hooks/useCreateMe.ts create mode 100644 client/src/hooks/useJoinMe.ts create mode 100644 client/src/hooks/useLeaveChat.ts create mode 100644 client/src/hooks/useMe.ts create mode 100644 client/src/hooks/useMePreview.ts create mode 100644 client/src/hooks/useSearchChats.ts create mode 100644 client/src/hooks/useSearchUsers.ts create mode 100644 client/src/hooks/useUpdateMe.ts create mode 100644 client/src/hooks/useUser.ts create mode 100644 client/src/stores/InitData.store.ts create mode 100644 client/src/stores/StartData.store.ts create mode 100644 client/src/utils/InitDataWrapper.tsx diff --git a/client/package-lock.json b/client/package-lock.json index e855fe7..5618de3 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.0.13", + "@tanstack/react-query": "^5.74.8", "@telegram-apps/sdk-react": "^3.1.2", "axios": "^1.8.3", "motion": "^12.5.0", @@ -1511,6 +1512,32 @@ "vite": "^5.2.0 || ^6" } }, + "node_modules/@tanstack/query-core": { + "version": "5.74.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.74.7.tgz", + "integrity": "sha512-X3StkN/Y6KGHndTjJf8H8th7AX4bKfbRpiVhVqevf0QWlxl6DhyJ0TYG3R0LARa/+xqDwzU9mA4pbJxzPCI29A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.74.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.74.8.tgz", + "integrity": "sha512-Hs3QHLYyyc/yUI8tS7CIM8vRmlm+2gPBiNsHzcDclvqZNCgTc0LdXRrZDhSxxhcnXJrIMnIcoNPwj9BshsCT+Q==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.74.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@telegram-apps/bridge": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@telegram-apps/bridge/-/bridge-2.4.0.tgz", diff --git a/client/package.json b/client/package.json index e935da6..54b199a 100644 --- a/client/package.json +++ b/client/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.0.13", + "@tanstack/react-query": "^5.74.8", "@telegram-apps/sdk-react": "^3.1.2", "axios": "^1.8.3", "motion": "^12.5.0", diff --git a/client/src/App.tsx b/client/src/App.tsx index ab71084..6e5f6e4 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -4,9 +4,7 @@ import { AboutFirstPage, AboutSecondPage } from "./pages/about.page"; import ChatPage from "./pages/chat.page"; import ChatsPage from "./pages/chats.page"; import { useCallback, useEffect } from "react"; -import { getMePreview } from "./api/api"; -import { backButton, initData } from "@telegram-apps/sdk-react"; -import useUserStore from "./stores/user.store"; +import { backButton } from "@telegram-apps/sdk-react"; import ProtectedRoute from "./components/protectedRoute.component"; import CurrentProfilePage from "./pages/currentProfile.page"; import EditProfilePage from "./pages/editProfile.page"; @@ -15,6 +13,8 @@ import InitialPage from "./pages/initial.page"; import InvitationPage from "./pages/invitation.page"; import ProfilePage from "./pages/profile.page"; import { AnimatePresence, motion } from "framer-motion"; +import useInitDataStore from "./stores/InitData.store"; +import InitDataWrapper from "./utils/InitDataWrapper"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -25,60 +25,49 @@ const pageVariants = { function App() { const navigate = useNavigate(); const location = useLocation(); - const userStore = useUserStore(); - - const checkAuth = async () => { - const userData = await getMePreview(initData.raw() ?? ""); - if (userData?.isRegistered) { - userStore.authenticate(); - userStore.setIsLoading(false); - userStore.updateUserData(userData); - } else { - userStore.setIsLoading(false); - } - }; const setBackButtonHandler = useCallback(() => { backButton.onClick(() => { navigate(-1); }); - }, []); + }, [navigate]); useEffect(() => { - checkAuth(); setBackButtonHandler(); - }, []); + }, [setBackButtonHandler]); return ( -
    - - - - }> - } /> - } /> - } /> - } /> - } /> - } /> - - } /> - } /> - } /> - } /> - - - -
    + +
    + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + + + +
    +
    ); } diff --git a/client/src/api/api.ts b/client/src/api/api.ts index b4142d1..46663c4 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -11,7 +11,7 @@ export const api = axios.create({ export const getChat = async ( initData: string, - chatId: string + chatId: string, ): Promise => { try { const response = await api.get(`/chats/id/${chatId}`, { @@ -26,14 +26,14 @@ export const getChat = async ( export const getChatPreview = async ( initData: string, - chatId: string + chatId: string, ): Promise => { try { const response = await api.get( `/chats/id/${chatId}/preview`, { headers: { "X-Api-Token": initData }, - } + }, ); return response.data; } catch (error) { @@ -43,7 +43,7 @@ export const getChatPreview = async ( }; export const getChats = async ( - initData: string + initData: string, ): Promise => { try { const response = await api.get(`/chats`, { @@ -59,7 +59,7 @@ export const getChats = async ( // userID – inner backend id export const getUserById = async ( initData: string, - userId: string + userId: string, ): Promise => { try { const response = await api.get(`/users/id/${userId}`, { @@ -86,7 +86,10 @@ export const getMe = async (initData: string): Promise => { export const postMe = async ( initData: string, - newData: Pick + newData: Pick< + UserData, + "firstName" | "lastName" | "bio" | "role" | "company" + >, ): Promise => { try { const response = await api.put(`/users/me`, newData, { @@ -104,7 +107,7 @@ export const createMe = async ( userData: Pick< UserData, "firstName" | "lastName" | "bio" | "role" | "company" - > + >, ): Promise => { try { const response = await api.post( @@ -134,7 +137,7 @@ export const joinMe = async ( headers: { "X-Api-Token": initData, }, - } + }, ); return response.status === 200; } catch (error) { @@ -146,7 +149,7 @@ export const joinMe = async ( export const searchInChat = async ( initData: string, chatId: string, - query: string + query: string, ): Promise => { try { const response = await api.get(`/chats/id/${chatId}/search`, { @@ -166,7 +169,7 @@ export const searchInChat = async ( export const searchChats = async ( initData: string, - query: string + query: string, ): Promise => { try { const response = await api.get(`/chats/search`, { @@ -186,7 +189,7 @@ export const searchChats = async ( export const leaveChat = async ( initData: string, - chatId: string + chatId: string, ): Promise => { try { const response = await api.post( @@ -196,7 +199,7 @@ export const leaveChat = async ( headers: { "X-Api-Token": initData, }, - } + }, ); return response.status === 200; } catch (error) { @@ -205,22 +208,8 @@ export const leaveChat = async ( } }; -export const getUserBioByTelegramUsername = async ( - telegramUsername: string -) => { - try { - const response = await axios.get( - `http://localhost:8000/proxy/?url=https://t.me/${telegramUsername}` - ); - return response.data; - } catch (error) { - console.error("Ошибка при получении био пользователя:", error); - return null; - } -}; - export const getMePreview = async ( - initData: string + initData: string, ): Promise => { try { const response = await api.get(`/users/me/preview`, { diff --git a/client/src/components/about.component.tsx b/client/src/components/about.component.tsx index b53e3b1..d7cd81d 100644 --- a/client/src/components/about.component.tsx +++ b/client/src/components/about.component.tsx @@ -14,7 +14,7 @@ const AboutComponent = (props: aboutComponentProps) => { About
    -
    +

    {props.contentText}

    diff --git a/client/src/components/profileView.component.tsx b/client/src/components/profileView.component.tsx deleted file mode 100644 index 9e2e6fc..0000000 --- a/client/src/components/profileView.component.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { initData } from "@telegram-apps/sdk-react"; -import { useEffect, useCallback } from "react"; -import { getMe } from "../api/api"; -import { handleImageError } from "../utils/imageErrorHandler"; -import InfoBlockComponent from "./infoBlock.component"; -import InfoParagraphComponent from "./infoParagraph.component"; -import useUserStore from "../stores/user.store"; -import DevImage from "../assets/dev.png"; -const ProfileViewComponent = () => { - const { userData, updateUserData } = useUserStore(); - - const fetchProfileData = useCallback(async () => { - const data = await getMe(initData.raw() ?? ""); - if (data) { - updateUserData(data); - } - }, []); - - useEffect(() => { - fetchProfileData(); - }, []); - - return ( - <> -
    - - -
    -

    - {userData.firstName} {userData.lastName} -

    -

    - {`@${userData.telegramUsername}`} -

    -
    -
    -
    - - - - -
    - ПОДРОБНЕЕ -
    - - - -
    - - ); -}; - -export default ProfileViewComponent; diff --git a/client/src/components/protectedRoute.component.tsx b/client/src/components/protectedRoute.component.tsx index df96055..9f0bf94 100644 --- a/client/src/components/protectedRoute.component.tsx +++ b/client/src/components/protectedRoute.component.tsx @@ -1,11 +1,11 @@ -import useUserStore from "../stores/user.store"; import { Navigate, Outlet } from "react-router-dom"; +import useMePreview from "../hooks/useMePreview"; const ProtectedRoute = () => { - const { isAuthenticated, isLoading } = useUserStore(); - if (isLoading) { + const { isPending, data } = useMePreview(); + if (isPending) { return null; } - if (!isAuthenticated) { + if (!data?.isRegistered) { return ; } return ; diff --git a/client/src/components/requireMembership.component.tsx b/client/src/components/requireMembership.component.tsx index 829865b..58cc4f1 100644 --- a/client/src/components/requireMembership.component.tsx +++ b/client/src/components/requireMembership.component.tsx @@ -1,8 +1,7 @@ -import { initData, initDataStartParam } from "@telegram-apps/sdk-react"; -import { getChatPreview } from "../api/api"; -import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import useInviteStore from "../stores/invite.store"; +import useInitDataStore from "../stores/InitData.store"; +import useChatPreview from "../hooks/useChatPreview"; +import { useEffect } from "react"; interface RequireMembershipComponentProps { children: React.ReactNode; @@ -10,40 +9,22 @@ interface RequireMembershipComponentProps { } const RequireMembershipComponent = (props: RequireMembershipComponentProps) => { const navigate = useNavigate(); + const { initDataStartParam } = useInitDataStore(); - const inviteStore = useInviteStore(); - - const initDataRaw = initData.raw(); - let chatID = props.chatID !== undefined ? props.chatID : initDataStartParam(); - const [isLoading, setIsLoading] = useState(true); - const [isMember, setIsMember] = useState(false); - - const checkMembership = async () => { - if (initDataStartParam() !== undefined && !inviteStore.showedOnce) { - chatID = initDataStartParam(); - inviteStore.setShowedOnce(); - } - const data = await getChatPreview(initDataRaw ?? "", chatID || ""); - if (data !== null && data?.isMember !== null) { - setIsMember(data.isMember); - } - setIsLoading(false); - }; + const chatID = props.chatID ?? initDataStartParam; + const { isPending, data } = useChatPreview(chatID || ""); useEffect(() => { - checkMembership(); - if (!isLoading) { - if (!isMember) { - navigate("/invite", { replace: true }); - } + if (!isPending && data && data.isMember === false) { + navigate("/invite", { replace: true }); } - }, [isLoading, isMember]); + }, [isPending, data, navigate]); - if (isLoading) { + if (isPending || !data || data.isMember === false) { return null; } - return isMember ? <>{props.children} : null; + return <>{props.children}; }; export default RequireMembershipComponent; diff --git a/client/src/components_v2/About.tsx b/client/src/components_v2/About.tsx new file mode 100644 index 0000000..616666c --- /dev/null +++ b/client/src/components_v2/About.tsx @@ -0,0 +1,42 @@ +interface aboutComponentProps { + onClick: () => void; + imageSrc: string; + contentText: string; + buttonText: string; +} + +const About = ({ + onClick, + imageSrc, + contentText, + buttonText, +}: aboutComponentProps) => { + return ( +
    +
    +
    +
    + About +
    + +
    +

    + {contentText} +

    +
    +
    + +
    + +
    +
    +
    + ); +}; + +export default About; diff --git a/client/src/components_v2/Button.tsx b/client/src/components_v2/Button.tsx new file mode 100644 index 0000000..925312f --- /dev/null +++ b/client/src/components_v2/Button.tsx @@ -0,0 +1,45 @@ +export type ButtonState = "active" | "disabled"; +export interface ButtonComponentProps { + content: React.ReactNode; + onClick: () => void; + state: "active" | "disabled" | "previewMessage"; +} +const ButtonComponent = (props: ButtonComponentProps) => { + const activeButtonStyle = + "bg-[#20C86E] rounded-[30px] text-white font-semibold"; + + const disabledButtonStyle = "bg-form rounded-[30px] text-main font-semibold"; + + const previewMessageUserStyle = + "bg-[#20C86E] rounded-[10px] text-white font-semibold px-[30px] py-[6px]"; + if (props.state === "active") { + return ( + + ); + } else if (props.state === "disabled") { + return ( + + ); + } else if (props.state === "previewMessage") { + return ( + + ); + } +}; + +export default ButtonComponent; diff --git a/client/src/components_v2/ChatHeader.tsx b/client/src/components_v2/ChatHeader.tsx new file mode 100644 index 0000000..24e514c --- /dev/null +++ b/client/src/components_v2/ChatHeader.tsx @@ -0,0 +1,33 @@ +import { handleImageError } from "../utils/imageErrorHandler"; +import DevImage from "../assets/dev.png"; + +interface chatHeaderComponentProps { + avatar: string; + name: string; + usersAmount: number; +} +const ChatHeaderComponent = (props: chatHeaderComponentProps) => { + return ( +
    +
  • + +
    +
    +

    + {props.name ?? " название чата "} +

    +

    + {props.usersAmount ?? "0"} участников +

    +
    +
    +
  • +
    + ); +}; + +export default ChatHeaderComponent; diff --git a/client/src/components_v2/ChatMemberCard.tsx b/client/src/components_v2/ChatMemberCard.tsx new file mode 100644 index 0000000..e012da7 --- /dev/null +++ b/client/src/components_v2/ChatMemberCard.tsx @@ -0,0 +1,33 @@ +import { getUserById } from "../api/api"; +import { UserData } from "../types/user.interface"; +import { useNavigate } from "react-router-dom"; +import { initData } from "@telegram-apps/sdk-react"; +import { useQuery } from "@tanstack/react-query"; + +const cardVariants = { + hidden: { opacity: 0, y: 20 }, + visible: (index: number) => ({ + opacity: 1, + y: 0, + transition: { + delay: index > 10 ? index * 0.01 : 11 * 0.01, + duration: 0.4, + ease: "easeOut", + }, + }), +}; + +const ChatMemberCard = ( + props: UserData & { + index: number; + animated?: boolean; + onAnimationComplete?: () => void; + }, +) => { + const navigate = useNavigate(); + const userData = useQuery(`use`, () => + getUserById(initData.raw() ?? "", props.id ?? ""), + ); +}; + +export default ChatMemberCard; diff --git a/client/src/hooks/useChat.ts b/client/src/hooks/useChat.ts new file mode 100644 index 0000000..3c9d4e2 --- /dev/null +++ b/client/src/hooks/useChat.ts @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; +import { getChat } from "../api/api"; +import useInitDataStore from "../stores/InitData.store"; + +const useChat = (chatId: string) => { + const { initData } = useInitDataStore(); + return useQuery({ + queryKey: [`chat/${chatId}`, initData, chatId], + queryFn: () => getChat(initData, chatId), + }); +}; + +export default useChat; diff --git a/client/src/hooks/useChatPreview.ts b/client/src/hooks/useChatPreview.ts new file mode 100644 index 0000000..d20f8f1 --- /dev/null +++ b/client/src/hooks/useChatPreview.ts @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; +import { getChatPreview } from "../api/api"; +import useInitDataStore from "../stores/InitData.store"; + +const useChatPreview = (chatId: string) => { + const { initData } = useInitDataStore(); + return useQuery({ + queryKey: [`chat/${chatId}/preview`, initData, chatId], + queryFn: () => getChatPreview(initData, chatId), + }); +}; + +export default useChatPreview; diff --git a/client/src/hooks/useChats.ts b/client/src/hooks/useChats.ts new file mode 100644 index 0000000..d17e5a4 --- /dev/null +++ b/client/src/hooks/useChats.ts @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; +import { getChats } from "../api/api"; +import useInitDataStore from "../stores/InitData.store"; + +const useChats = () => { + const { initData } = useInitDataStore(); + return useQuery({ + queryKey: ["chats", initData], + queryFn: () => getChats(initData), + }); +}; + +export default useChats; diff --git a/client/src/hooks/useCreateMe.ts b/client/src/hooks/useCreateMe.ts new file mode 100644 index 0000000..586e014 --- /dev/null +++ b/client/src/hooks/useCreateMe.ts @@ -0,0 +1,27 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import useInitDataStore from "../stores/InitData.store"; +import { createMe } from "../api/api"; +import { UserData } from "../types/user.interface"; + +const useCreateMe = () => { + const { initData } = useInitDataStore(); + + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ( + profileData: Pick< + UserData, + "firstName" | "lastName" | "bio" | "role" | "company" + >, + ) => createMe(initData, profileData), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["user/me", initData] }); + queryClient.invalidateQueries({ + queryKey: ["user/me/preview", initData], + }); + }, + }); +}; + +export default useCreateMe; diff --git a/client/src/hooks/useJoinMe.ts b/client/src/hooks/useJoinMe.ts new file mode 100644 index 0000000..abc4b9a --- /dev/null +++ b/client/src/hooks/useJoinMe.ts @@ -0,0 +1,26 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import useInitDataStore from "../stores/InitData.store"; +import { joinMe } from "../api/api"; + +const useJoinMe = () => { + const queryClient = useQueryClient(); + const { initData } = useInitDataStore(); + + return useMutation({ + mutationFn: (chatId: string) => joinMe(initData, chatId), + onSuccess: async (_, chatId) => { + await queryClient.refetchQueries({ + predicate: (query) => + Array.isArray(query.queryKey) && + query.queryKey[0] === "chats/preview", + }); + queryClient.invalidateQueries({ queryKey: ["chats"] }); + queryClient.invalidateQueries({ queryKey: [`chat/${chatId}`, chatId] }); + queryClient.invalidateQueries({ + queryKey: [`chat/${chatId}/preview`, initData, chatId], + }); + }, + }); +}; + +export default useJoinMe; diff --git a/client/src/hooks/useLeaveChat.ts b/client/src/hooks/useLeaveChat.ts new file mode 100644 index 0000000..8da4f7d --- /dev/null +++ b/client/src/hooks/useLeaveChat.ts @@ -0,0 +1,21 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { leaveChat } from "../api/api"; +import useInitDataStore from "../stores/InitData.store"; +import { ChatPreviewData } from "../types/user.interface"; + +const useLeaveChat = () => { + const { initData } = useInitDataStore(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (chatId: string) => leaveChat(initData, chatId), + onSuccess: (_, chatId) => { + queryClient.setQueryData( + ["chats/preview", initData, ""], + (old) => old?.filter((chat) => chat.id !== chatId) ?? [], + ); + }, + }); +}; + +export default useLeaveChat; diff --git a/client/src/hooks/useMe.ts b/client/src/hooks/useMe.ts new file mode 100644 index 0000000..4de5a7b --- /dev/null +++ b/client/src/hooks/useMe.ts @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; +import useInitDataStore from "../stores/InitData.store"; +import { getMe } from "../api/api"; + +const useMe = () => { + const { initData } = useInitDataStore(); + return useQuery({ + queryKey: ["user/me", initData], + queryFn: () => getMe(initData), + }); +}; + +export default useMe; diff --git a/client/src/hooks/useMePreview.ts b/client/src/hooks/useMePreview.ts new file mode 100644 index 0000000..15bbd27 --- /dev/null +++ b/client/src/hooks/useMePreview.ts @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; +import useInitDataStore from "../stores/InitData.store"; +import { getMePreview } from "../api/api"; + +const useMePreview = () => { + const { initData } = useInitDataStore(); + return useQuery({ + queryKey: [`users/me/preview`, initData], + queryFn: () => getMePreview(initData), + }); +}; + +export default useMePreview; diff --git a/client/src/hooks/useSearchChats.ts b/client/src/hooks/useSearchChats.ts new file mode 100644 index 0000000..5a297ce --- /dev/null +++ b/client/src/hooks/useSearchChats.ts @@ -0,0 +1,15 @@ +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { searchChats } from "../api/api"; +import useInitDataStore from "../stores/InitData.store"; + +const useSearchChats = (query: string) => { + const { initData } = useInitDataStore(); + + return useQuery({ + queryKey: ["chats/preview", initData, query], + queryFn: () => searchChats(initData, query), + placeholderData: keepPreviousData, + }); +}; + +export default useSearchChats; diff --git a/client/src/hooks/useSearchUsers.ts b/client/src/hooks/useSearchUsers.ts new file mode 100644 index 0000000..0acdeaa --- /dev/null +++ b/client/src/hooks/useSearchUsers.ts @@ -0,0 +1,15 @@ +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { searchInChat } from "../api/api"; +import useInitDataStore from "../stores/InitData.store"; + +const useSearchUsersInChat = (chatId: string, query: string) => { + const { initData } = useInitDataStore(); + + return useQuery({ + queryKey: [`chat/${chatId}`, initData, chatId, query], + queryFn: () => searchInChat(initData, chatId, query), + placeholderData: keepPreviousData, + }); +}; + +export default useSearchUsersInChat; diff --git a/client/src/hooks/useUpdateMe.ts b/client/src/hooks/useUpdateMe.ts new file mode 100644 index 0000000..930209d --- /dev/null +++ b/client/src/hooks/useUpdateMe.ts @@ -0,0 +1,27 @@ +import { useQueryClient, useMutation } from "@tanstack/react-query"; +import { postMe } from "../api/api"; +import useInitDataStore from "../stores/InitData.store"; +import { UserData } from "../types/user.interface"; + +const useUpdateMe = () => { + const { initData } = useInitDataStore(); + + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ( + profileData: Pick< + UserData, + "firstName" | "lastName" | "bio" | "role" | "company" + >, + ) => postMe(initData, profileData), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["user/me", initData] }); + queryClient.invalidateQueries({ + queryKey: ["user/me/preview", initData], + }); + }, + }); +}; + +export default useUpdateMe; diff --git a/client/src/hooks/useUser.ts b/client/src/hooks/useUser.ts new file mode 100644 index 0000000..f0d7ffc --- /dev/null +++ b/client/src/hooks/useUser.ts @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; +import { getUserById } from "../api/api"; +import useInitDataStore from "../stores/InitData.store"; + +const useUser = (userId: string) => { + const { initData } = useInitDataStore(); + return useQuery({ + queryKey: [`user/${userId}`, initData, userId], + queryFn: () => getUserById(initData, userId), + }); +}; + +export default useUser; diff --git a/client/src/main.tsx b/client/src/main.tsx index 1a3b0a0..d3e8bd7 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -4,11 +4,15 @@ import "./index.css"; import App from "./App.tsx"; import initializeApplication from "./utils/initApp.tsx"; import { BrowserRouter } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; initializeApplication(); +const queryClient = new QueryClient(); createRoot(document.getElementById("root")!).render( - + + + - + , ); diff --git a/client/src/pages/chat.page.tsx b/client/src/pages/chat.page.tsx index 1a10e87..dcdcee4 100644 --- a/client/src/pages/chat.page.tsx +++ b/client/src/pages/chat.page.tsx @@ -3,14 +3,13 @@ import EBBComponent from "../components/enableBackButtonComponent"; import RequireMembershipComponent from "../components/requireMembership.component"; import SearchBarComponent from "../components/searchBar.component"; import ChatPreviewComponent from "../components/chatPreview.component"; -import { getChat, getChatPreview, searchInChat } from "../api/api"; -import { initData } from "@telegram-apps/sdk-react"; -import { ChatData, ChatPreviewData } from "../types/user.interface"; import { useEffect, useRef, useState } from "react"; import useChatSearchStore from "../stores/chatSearch.store"; import ChatMemberCardComponent from "../components/chatMember.card.component"; import { motion } from "motion/react"; import NotFound from "../assets/notFound.svg"; +import useSearchUsersInChat from "../hooks/useSearchUsers"; +import useChatPreview from "../hooks/useChatPreview"; const containerVariants = { visible: { transition: { @@ -20,57 +19,16 @@ const containerVariants = { }; const ChatPage = () => { const { chatId } = useParams(); - const [opened, setOpened] = useState(false); - const { searchQuery, setSearchQuery, chatID, setChatID, setScroll, scroll } = - useChatSearchStore(); - - const [loading, isLoading] = useState(true); - - const [chatData, setChatData] = useState({ - name: null, - id: chatId ?? "", - avatar: null, - telegramId: "", - users: [], - }); + const [loadedFirstTime, setLoadedFirstTime] = useState(false); - const [previewChatData, setPreviewChatData] = useState({ - avatar: null, - usersAmount: null, - name: null, - id: chatId ?? "", - telegramId: "", - isMember: null, - }); - - const fetchChatPreview = async () => { - const chatData = await getChatPreview(initData.raw() ?? "", chatId ?? ""); - if (chatData) { - setPreviewChatData(chatData); - } - isLoading(false); - }; - - const fetchChatData = async () => { - const chatData = await getChat(initData.raw() ?? "", chatId ?? ""); - if (chatData) { - setChatData(chatData); - } - }; + const { searchQuery, setSearchQuery, setScroll, scroll } = + useChatSearchStore(); - const searchUsers = async () => { - const searchResponse = await searchInChat( - initData.raw() ?? "", - chatId ?? "", - searchQuery - ); - if (searchResponse) { - setChatData({ ...chatData, users: searchResponse }); - } else { - setChatData({ ...chatData, users: [] }); - } - isLoading(false); - }; + const { data: chatData, isPending } = useSearchUsersInChat( + chatId ?? "", + searchQuery, + ); + const { data: previewChatData, isSuccess } = useChatPreview(chatId ?? ""); const scrollContainerRef = useRef(null); const handleScroll = () => { @@ -79,39 +37,9 @@ const ChatPage = () => { } }; - useEffect(() => { - if (chatId !== chatID) { - setChatID(chatId ?? ""); - setSearchQuery(""); - } - fetchChatPreview(); - if (previewChatData.isMember) { - fetchChatData(); - } - searchUsers(); - }, [chatId]); - - useEffect(() => { - if (previewChatData.isMember) { - searchUsers(); - } - setOpened(true); - }, [searchQuery]); - - useEffect(() => { - if (previewChatData.isMember) { - searchUsers(); - } - }, [loading]); - - useEffect(() => { - setOpened(false); - }, []); - useEffect(() => { const timer = setTimeout(() => { if (scrollContainerRef.current) { - console.log(scroll); scrollContainerRef.current.scrollTo({ top: scroll, behavior: "smooth", @@ -120,7 +48,14 @@ const ChatPage = () => { }, 100); return () => clearTimeout(timer); - }, [loading]); + }, [isPending]); + + useEffect(() => { + if (!isPending) { + setLoadedFirstTime(true); + } + }, [isPending]); + return ( @@ -129,29 +64,9 @@ const ChatPage = () => { onScroll={handleScroll} className="max-w-[95%] max-h-[100vh] overflow-auto scroll-container mx-auto px-4" > - {previewChatData.isMember === null && ( -
  • -
    -
    -
    -

    -

    -
    -
    -
    -
  • - )} - {previewChatData.isMember !== null && ( + {isSuccess && ( { placeholder="Поиск участников" className="mt-[20px]" /> - {opened && ( + {loadedFirstTime && (
      - {chatData.users.map((user, index) => ( + {chatData?.map((user, index) => ( { ))}
    )} - {!opened && ( + {!loadedFirstTime && ( - {chatData.users.map((user, index) => ( + {chatData?.map((user, index) => ( setOpened(true) + index === chatData.length - 1 + ? () => setLoadedFirstTime(true) : undefined } /> ))} )} - {chatData.users.length === 0 && !loading && ( + {!chatData && !isPending && (
    diff --git a/client/src/pages/chats.page.tsx b/client/src/pages/chats.page.tsx index c23de3b..9e3ad42 100644 --- a/client/src/pages/chats.page.tsx +++ b/client/src/pages/chats.page.tsx @@ -1,18 +1,18 @@ -import { useEffect, useState, useCallback, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import ProfileComponent from "../components/profile.component"; import SearchBarComponent from "../components/searchBar.component"; import { ChatPreviewData } from "../types/user.interface"; -import { getMe, leaveChat, searchChats } from "../api/api"; -import { initData, openTelegramLink, popup } from "@telegram-apps/sdk-react"; +import { openTelegramLink, popup } from "@telegram-apps/sdk-react"; import ChatPreviewComponent from "../components/chatPreview.component"; import DBBComponent from "../components/disableBackButton.component"; import { Outlet } from "react-router-dom"; import useChatsSearchStore from "../stores/chatsSearch.store"; import { BOT_USERNAME } from "../shared/constants"; -import useUserStore from "../stores/user.store"; import NotFound from "../assets/notFound.svg"; import AddButton from "../assets/add_green.svg"; import { motion } from "motion/react"; +import useLeaveChat from "../hooks/useLeaveChat"; +import useSearchChats from "../hooks/useSearchChats"; const containerVariants = { visible: { transition: { @@ -21,12 +21,15 @@ const containerVariants = { }, }; const ChatsPage = () => { - const [opened, setOpened] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [chats, setChats] = useState([]); + const useLeaveChatMutation = useLeaveChat(); + const { searchQuery, setSearchQuery, scroll, setScroll } = useChatsSearchStore(); - const userStore = useUserStore(); + + const { data: chats, isPending } = useSearchChats(searchQuery); + + const [loadedFirstTime, setLoadedFirstTime] = useState(false); + const deleteHandler = async (chatPreviewData: ChatPreviewData) => { popup .open({ @@ -39,54 +42,18 @@ const ChatsPage = () => { }) .then(async (buttonId) => { if (buttonId === "Ok") { - const response = await leaveChat( - initData.raw() ?? "", - chatPreviewData.id ?? "" - ); - if (response) { - setChats(chats.filter((chat) => chat.id !== chatPreviewData.id)); - } + await useLeaveChatMutation.mutateAsync(chatPreviewData.id ?? ""); } }); }; - const fetchChats = async (query: string) => { - try { - const data = await searchChats(initData.raw() ?? "", query); - if (data) { - setChats(data); - } else { - setChats([]); - } - } catch (error) { - console.error("Ошибка загрузки чатов:", error); - } - setIsLoading(false); - }; - - const fetchUserData = useCallback(async () => { - const userData = await getMe(initData.raw() ?? ""); - if (userData) { - userStore.updateUserData(userData); - } - }, []); - - useEffect(() => { - setOpened(false); - fetchUserData(); - }, []); - - useEffect(() => { - fetchChats(searchQuery); - setOpened(true); - }, [searchQuery]); - const scrollContainerRef = useRef(null); const handleScroll = () => { if (scrollContainerRef.current) { setScroll(scrollContainerRef.current.scrollTop); } }; + useEffect(() => { const timer = setTimeout(() => { if (scrollContainerRef.current) { @@ -99,7 +66,13 @@ const ChatsPage = () => { }, 100); return () => clearTimeout(timer); - }, [isLoading]); + }, [isPending]); + + useEffect(() => { + if (!isPending) { + setLoadedFirstTime(true); + } + }, [isPending]); return ( <> @@ -128,9 +101,9 @@ const ChatsPage = () => { placeholder="Поиск" className="mt-[15px]" /> - {opened && ( + {loadedFirstTime && (
      - {chats.map((chat, index) => + {chats?.map((chat, index) => index !== chats.length - 1 ? ( { deleteHandler={() => deleteHandler(chat)} index={index} /> - ) + ), )}
    )} - {!opened && ( + {!loadedFirstTime && ( - {chats.map((chat, index) => + {chats?.map((chat, index) => index !== chats.length - 1 ? ( { animated onAnimationComplete={ index === chats.length - 1 - ? () => setOpened(true) + ? () => setLoadedFirstTime(true) : undefined } /> @@ -179,20 +152,15 @@ const ChatsPage = () => { deleteHandler={() => deleteHandler(chat)} index={index} animated - onAnimationComplete={ - index === chats.length - 1 - ? () => setOpened(true) - : undefined - } /> - ) + ), )} )} - {chats.length === 0 && !isLoading && ( + + {!chats && !isPending && (
    -

    Тут пока ничего нет diff --git a/client/src/pages/currentProfile.page.tsx b/client/src/pages/currentProfile.page.tsx index a7d4352..e424db9 100644 --- a/client/src/pages/currentProfile.page.tsx +++ b/client/src/pages/currentProfile.page.tsx @@ -1,34 +1,20 @@ -import { initData } from "@telegram-apps/sdk-react"; -import { useEffect, useCallback } from "react"; -import { getMe } from "../api/api"; import { handleImageError } from "../utils/imageErrorHandler"; import InfoBlockComponent from "../components/infoBlock.component"; import InfoParagraphComponent from "../components/infoParagraph.component"; -import useUserStore from "../stores/user.store"; import { useNavigate } from "react-router-dom"; import EBBComponent from "../components/enableBackButtonComponent"; import DevImage from "../assets/dev.png"; import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; +import useMe from "../hooks/useMe"; const CurrentProfilePage = () => { - const { userData, updateUserData } = useUserStore(); + const { data: userData } = useMe(); const navigate = useNavigate(); const goToEditProfilePage = () => { navigate("/profile/edit"); }; - const fetchProfileData = useCallback(async () => { - const data = await getMe(initData.raw() ?? ""); - if (data) { - updateUserData(data); - } - }, []); - - useEffect(() => { - fetchProfileData(); - }, []); - return (
    @@ -36,15 +22,15 @@ const CurrentProfilePage = () => {

    - {userData.firstName} {userData.lastName} + {userData?.firstName} {userData?.lastName}

    - {`@${userData.telegramUsername}`} + {`@${userData?.telegramUsername}`}

    @@ -53,18 +39,18 @@ const CurrentProfilePage = () => {
    ПОДРОБНЕЕ
    diff --git a/client/src/pages/editProfile.page.tsx b/client/src/pages/editProfile.page.tsx index 51dd082..28af210 100644 --- a/client/src/pages/editProfile.page.tsx +++ b/client/src/pages/editProfile.page.tsx @@ -3,21 +3,34 @@ import InputFieldComponent from "../components/inputField.component"; import TextareaFieldComponent from "../components/textareaField.component"; import { UserData, ProfileUserData } from "../types/user.interface"; import { handleImageError } from "../utils/imageErrorHandler"; -import useUserStore from "../stores/user.store"; -import { getMe, postMe } from "../api/api"; -import { initData } from "@telegram-apps/sdk-react"; import { useNavigate } from "react-router-dom"; import EBBComponent from "../components/enableBackButtonComponent"; import DevImage from "../assets/dev.png"; import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; import ButtonComponent from "../components/button.component"; -import { retrieveLaunchParams } from "@telegram-apps/sdk-react"; +import useInitDataStore from "../stores/InitData.store"; +import useMe from "../hooks/useMe"; +import useUpdateMe from "../hooks/useUpdateMe"; const EditProfilePage = () => { - const launchParams = retrieveLaunchParams(); + const updateMeMutation = useUpdateMe(); + + const { launchParams } = useInitDataStore(); const navigate = useNavigate(); - const { userData, updateUserData } = useUserStore(); + const { data } = useMe(); + const userData = { + firstName: data?.firstName ?? null, + lastName: data?.lastName ?? null, + company: data?.company ?? null, + role: data?.role ?? null, + bio: data?.bio ?? null, + avatar: data?.avatar ?? null, + id: data?.id ?? null, + telegramId: data?.telegramId ?? null, + telegramUsername: data?.telegramUsername ?? null, + }; + const [profileData, setProfileData] = useState< Omit >({ ...userData }); @@ -39,7 +52,7 @@ const EditProfilePage = () => { newProfileData: Pick< UserData, "firstName" | "lastName" | "bio" | "role" | "company" - > + >, ) => { const isFilled = profileData.firstName?.trim() != "" && @@ -63,31 +76,12 @@ const EditProfilePage = () => { const goBack = () => { navigate(-1); }; - const updateProfile = async () => { - const newUserData = await postMe(initData.raw() ?? "", profileData); - if (newUserData) { - updateUserData(newUserData); - setIsChanged(false); - goBack(); - } - }; useEffect(() => { setIsChanged(isProfileChanged(profileData)); setIsFilled(isProfileFilled(profileData)); }, [profileData]); - useEffect(() => { - const fetchProfileData = async () => { - const data = await getMe(initData.raw() ?? ""); - if (data) { - updateUserData(data); - setProfileData(data); - } - }; - fetchProfileData(); - }, []); - const [iosKeyboardOpen, setIosKeyboardOpen] = useState(false); const [focusedFieldsCount, setFocusedFiledsCount] = useState(0); const onFocusHandler = () => { @@ -97,7 +91,7 @@ const EditProfilePage = () => { setFocusedFiledsCount(focusedFieldsCount - 1); }; const handleFieldsCount = () => { - if (launchParams.tgWebAppPlatform === "ios") { + if (launchParams?.tgWebAppPlatform === "ios") { if (focusedFieldsCount > 0) { setIosKeyboardOpen(true); } @@ -106,7 +100,24 @@ const EditProfilePage = () => { } } }; + + const handleClick = async () => { + if (!isFilled) return; + + if (isChanged) { + try { + await updateMeMutation.mutateAsync(profileData); + goBack(); + } catch (error) { + console.error("Ошибка при обновлении профиля", error); + } + } else { + goBack(); + } + }; + useEffect(handleFieldsCount, [focusedFieldsCount]); + return (
    { value={profileData.bio ?? ""} maxLength={MAX_TEXTAREA_LENGTH} /> - {launchParams.tgWebAppPlatform === "ios" && ( + {launchParams?.tgWebAppPlatform === "ios" && (
    { - if (isFilled) { - if (isChanged) { - updateProfile(); - } else { - goBack(); - } - } - }} + handleClick={handleClick} state={isChanged && isFilled ? "active" : "disabled"} />
    )} - {launchParams.tgWebAppPlatform !== "ios" && ( + {launchParams?.tgWebAppPlatform !== "ios" && (
    )}

    - {launchParams.tgWebAppPlatform !== "ios" && ( + {launchParams?.tgWebAppPlatform !== "ios" && (
    { - if (isFilled) { - if (isChanged) { - updateProfile(); - } else { - goBack(); - } - } - }} + handleClick={handleClick} state={isChanged && isFilled ? "active" : "disabled"} />
    diff --git a/client/src/pages/initial.page.tsx b/client/src/pages/initial.page.tsx index 3219431..f7bf7b2 100644 --- a/client/src/pages/initial.page.tsx +++ b/client/src/pages/initial.page.tsx @@ -1,16 +1,19 @@ -import { initDataStartParam } from "@telegram-apps/sdk-react"; import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; +import useInitDataStore from "../stores/InitData.store"; const InitialPage = () => { - const startData = initDataStartParam(); + const { initDataStartParam: startParam } = useInitDataStore(); const navigate = useNavigate(); useEffect(() => { - if (startData !== undefined && startData.length > 0) { + if ( + startParam !== undefined && + startParam !== null && + startParam.length > 0 + ) { navigate("/chats", { replace: true }); - - navigate(`/chat/${startData}`); + navigate(`/chat/${startParam}`); } else { navigate("/chats", { replace: true }); } diff --git a/client/src/pages/invitation.page.tsx b/client/src/pages/invitation.page.tsx index 26885b5..b2c7a80 100644 --- a/client/src/pages/invitation.page.tsx +++ b/client/src/pages/invitation.page.tsx @@ -1,35 +1,23 @@ -import { useEffect, useState } from "react"; -import { ChatPreviewData } from "../types/user.interface"; import { handleImageError } from "../utils/imageErrorHandler"; -import { getChatPreview, joinMe } from "../api/api"; -import { initData, initDataStartParam } from "@telegram-apps/sdk-react"; import EBBComponent from "../components/enableBackButtonComponent"; import { useNavigate } from "react-router-dom"; import DevImage from "../assets/dev.png"; +import useInitDataStore from "../stores/InitData.store"; +import useJoinMe from "../hooks/useJoinMe"; +import useChatPreview from "../hooks/useChatPreview"; const InvitationPage = () => { const navigate = useNavigate(); - const chatId = initDataStartParam() ?? ""; + const { initDataStartParam: chatId } = useInitDataStore(); - const accept = async () => { - const response = await joinMe(initData.raw() ?? "", chatId); - if (response) { - navigate("/chats", { replace: true }); - navigate(`/chat/${chatId}`); - } + const joinMeMutation = useJoinMe(); + const handleAcceptInvitation = async () => { + await joinMeMutation.mutateAsync(chatId ?? ""); + navigate("/chats", { replace: true }); + navigate(`/chat/${chatId}`); }; - const [chatData, setChatData] = useState(); + const { data: chatData } = useChatPreview(chatId ?? ""); - const fetchChatData = async () => { - const fetchedChatData = await getChatPreview(initData.raw() ?? "", chatId); - if (fetchedChatData) { - setChatData(fetchedChatData); - } - }; - - useEffect(() => { - fetchChatData(); - }, [chatData]); return (
    @@ -40,10 +28,7 @@ const InvitationPage = () => {

    -
    +
    {
    diff --git a/client/src/pages/profile.page.tsx b/client/src/pages/profile.page.tsx index 9ca867e..e7a8233 100644 --- a/client/src/pages/profile.page.tsx +++ b/client/src/pages/profile.page.tsx @@ -1,28 +1,16 @@ -import { initData, openTelegramLink } from "@telegram-apps/sdk-react"; -import { useState, useEffect } from "react"; +import { openTelegramLink } from "@telegram-apps/sdk-react"; import { useParams } from "react-router-dom"; -import { getUserById } from "../api/api"; import EBBComponent from "../components/enableBackButtonComponent"; import InfoBlockComponent from "../components/infoBlock.component"; import InfoParagraphComponent from "../components/infoParagraph.component"; -import { UserData } from "../types/user.interface"; import { handleImageError } from "../utils/imageErrorHandler"; import DevImage from "../assets/dev.png"; import ButtonComponent from "../components/button.component"; import TGWhite from "../assets/tg_white.svg"; +import useUser from "../hooks/useUser"; const ProfilePage = () => { const { userId } = useParams(); - const [userData, setUserData] = useState(); - const fetchProfileData = async () => { - const data = await getUserById(initData.raw() ?? "", userId ?? ""); - if (data) { - setUserData(data); - } - }; - - useEffect(() => { - fetchProfileData(); - }, []); + const { data: userData } = useUser(userId ?? ""); return ( diff --git a/client/src/pages/registration.page.tsx b/client/src/pages/registration.page.tsx index 0f63abe..c56bcba 100644 --- a/client/src/pages/registration.page.tsx +++ b/client/src/pages/registration.page.tsx @@ -1,34 +1,37 @@ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect } from "react"; import InputFieldComponent from "../components/inputField.component"; import TextareaFieldComponent from "../components/textareaField.component"; import { UserData } from "../types/user.interface"; import { handleImageError } from "../utils/imageErrorHandler"; import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; -import { - initData, - initDataStartParam, - initDataUser, - retrieveLaunchParams, -} from "@telegram-apps/sdk-react"; import EBBComponent from "../components/enableBackButtonComponent"; -import { createMe, getMePreview, joinMe } from "../api/api"; import { useNavigate } from "react-router-dom"; -import useUserStore from "../stores/user.store"; import DevImage from "../assets/dev.png"; import ButtonComponent from "../components/button.component"; +import useStartParamStore from "../stores/StartData.store"; +import useInitDataStore from "../stores/InitData.store"; +import useCreateMe from "../hooks/useCreateMe"; +import useJoinMe from "../hooks/useJoinMe"; +import useMePreview from "../hooks/useMePreview"; const RegistrationPage = () => { - const launchParams = retrieveLaunchParams(); + //mutations + const createMeMutation = useCreateMe(); + const joinMeMutation = useJoinMe(); - const startParam = initDataStartParam(); - const userStore = useUserStore(); const navigate = useNavigate(); - const avatar = initDataUser()?.photo_url; + + //tg params + const { startParam } = useStartParamStore(); + const { initDataUser, launchParams } = useInitDataStore(); + const avatar = initDataUser?.photo_url; + + //form validation data const [profileData, setProfileData] = useState< Pick >({ - firstName: initDataUser()?.first_name || "", - lastName: initDataUser()?.last_name || "", + firstName: initDataUser?.first_name || "", + lastName: initDataUser?.last_name || "", company: "", role: "", bio: "", @@ -54,45 +57,32 @@ const RegistrationPage = () => { [field]: value, })); }; - const checkAuth = async () => { - userStore.setIsLoading(true); - const userData = await getMePreview(initData.raw() ?? ""); - if (userData?.isRegistered) { - userStore.authenticate(); - userStore.setIsLoading(false); - userStore.updateUserData(userData); - } else { - userStore.setIsLoading(false); - } - }; - const registerUser = async () => { - const response = await createMe(initData.raw() ?? "", profileData); - if (response) { - checkAuth(); + const handleRegister = async () => { + try { + await createMeMutation.mutateAsync(profileData); navigate("/chats", { replace: true }); - if (startParam !== undefined && startParam.length > 0) { - const joinResponse = await joinMe(initData.raw() ?? "", startParam); - if (joinResponse) { - navigate(`/chat/${startParam}`); - } + await joinMeMutation.mutateAsync(startParam); + navigate(`/chat/${startParam}`); + } else { + navigate("/chats", { replace: true }); } + } catch (error) { + console.error("Ошибка при создании пользователя", error); } }; - const fetchBio = useCallback(async () => { - const data = await getMePreview(initData.raw() ?? ""); - if (data) { + const { data: preview } = useMePreview(); + useEffect(() => { + if (preview?.bio) { setProfileData((prevState) => ({ ...prevState, - bio: data.bio, + bio: preview.bio, })); } - }, []); - useEffect(() => { - fetchBio(); - }, []); + }, [preview?.bio]); + useEffect(() => { setIsFilled(isProfileFilled()); }, [profileData]); @@ -106,7 +96,7 @@ const RegistrationPage = () => { setFocusedFiledsCount(focusedFieldsCount - 1); }; const handleFieldsCount = () => { - if (launchParams.tgWebAppPlatform === "ios") { + if (launchParams?.tgWebAppPlatform === "ios") { if (focusedFieldsCount > 0) { setIosKeyboardOpen(true); } @@ -175,20 +165,20 @@ const RegistrationPage = () => { />
    - {launchParams.tgWebAppPlatform !== "ios" && ( + {launchParams?.tgWebAppPlatform !== "ios" && (
    )} - {launchParams.tgWebAppPlatform === "ios" && ( + {launchParams?.tgWebAppPlatform === "ios" && (
    { - if (isFilled) registerUser(); + if (isFilled) handleRegister(); }} className="flex w-full justify-center pt-[20px]" > { - if (isFilled) registerUser(); + if (isFilled) handleRegister(); }} state={isFilled ? "active" : "disabled"} /> @@ -196,12 +186,12 @@ const RegistrationPage = () => { )}
    - {launchParams.tgWebAppPlatform !== "ios" && ( + {launchParams?.tgWebAppPlatform !== "ios" && (
    { - if (isFilled) registerUser(); + if (isFilled) handleRegister(); }} state={isFilled ? "active" : "disabled"} /> diff --git a/client/src/stores/InitData.store.ts b/client/src/stores/InitData.store.ts new file mode 100644 index 0000000..536f781 --- /dev/null +++ b/client/src/stores/InitData.store.ts @@ -0,0 +1,41 @@ +// stores/InitData.store.ts +import { create } from "zustand"; +import { + initData, + initDataStartParam, + initDataUser, + retrieveLaunchParams, + RetrieveLPResult, +} from "@telegram-apps/sdk-react"; + +interface InitDataState { + initData: string; + initDataUser: ReturnType | null; + initDataStartParam: ReturnType | null; + launchParams: RetrieveLPResult | null; + initialized: boolean; + initialize: () => void; +} + +const useInitDataStore = create((set) => ({ + initData: "", + initDataUser: null, + initialized: false, + initDataStartParam: null, + launchParams: null, + initialize: () => { + const raw = initData.raw(); + const user = initDataUser(); + const startParam = initDataStartParam(); + const retrievedLaunchParams = retrieveLaunchParams(); + set({ + initData: raw ?? "", + initDataUser: user ?? null, + initDataStartParam: startParam ?? null, + initialized: true, + launchParams: retrievedLaunchParams ?? null, + }); + }, +})); + +export default useInitDataStore; diff --git a/client/src/stores/StartData.store.ts b/client/src/stores/StartData.store.ts new file mode 100644 index 0000000..f219e71 --- /dev/null +++ b/client/src/stores/StartData.store.ts @@ -0,0 +1,8 @@ +import { create } from "zustand"; +import { initDataStartParam } from "@telegram-apps/sdk-react"; + +const useStartParamStore = create(() => ({ + startParam: initDataStartParam(), +})); + +export default useStartParamStore; diff --git a/client/src/utils/InitDataWrapper.tsx b/client/src/utils/InitDataWrapper.tsx new file mode 100644 index 0000000..9a89ae6 --- /dev/null +++ b/client/src/utils/InitDataWrapper.tsx @@ -0,0 +1,16 @@ +import { useEffect } from "react"; +import useInitDataStore from "../stores/InitData.store"; + +const InitDataWrapper = ({ children }: { children: React.ReactNode }) => { + const { initialize, initialized } = useInitDataStore(); + + useEffect(() => { + initialize(); + }, []); + + if (!initialized) return null; + + return <>{children}; +}; + +export default InitDataWrapper; From 946078d06bcc029940296fc5cae215b5251ac7a3 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 1 May 2025 15:07:38 +0300 Subject: [PATCH 07/78] remove trash components (will do later) --- client/src/App.tsx | 1 - client/src/components_v2/About.tsx | 42 ------------------- client/src/components_v2/Button.tsx | 45 --------------------- client/src/components_v2/ChatHeader.tsx | 33 --------------- client/src/components_v2/ChatMemberCard.tsx | 33 --------------- 5 files changed, 154 deletions(-) delete mode 100644 client/src/components_v2/About.tsx delete mode 100644 client/src/components_v2/Button.tsx delete mode 100644 client/src/components_v2/ChatHeader.tsx delete mode 100644 client/src/components_v2/ChatMemberCard.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 6e5f6e4..55a16fb 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -13,7 +13,6 @@ import InitialPage from "./pages/initial.page"; import InvitationPage from "./pages/invitation.page"; import ProfilePage from "./pages/profile.page"; import { AnimatePresence, motion } from "framer-motion"; -import useInitDataStore from "./stores/InitData.store"; import InitDataWrapper from "./utils/InitDataWrapper"; const pageVariants = { diff --git a/client/src/components_v2/About.tsx b/client/src/components_v2/About.tsx deleted file mode 100644 index 616666c..0000000 --- a/client/src/components_v2/About.tsx +++ /dev/null @@ -1,42 +0,0 @@ -interface aboutComponentProps { - onClick: () => void; - imageSrc: string; - contentText: string; - buttonText: string; -} - -const About = ({ - onClick, - imageSrc, - contentText, - buttonText, -}: aboutComponentProps) => { - return ( -
    -
    -
    -
    - About -
    - -
    -

    - {contentText} -

    -
    -
    - -
    - -
    -
    -
    - ); -}; - -export default About; diff --git a/client/src/components_v2/Button.tsx b/client/src/components_v2/Button.tsx deleted file mode 100644 index 925312f..0000000 --- a/client/src/components_v2/Button.tsx +++ /dev/null @@ -1,45 +0,0 @@ -export type ButtonState = "active" | "disabled"; -export interface ButtonComponentProps { - content: React.ReactNode; - onClick: () => void; - state: "active" | "disabled" | "previewMessage"; -} -const ButtonComponent = (props: ButtonComponentProps) => { - const activeButtonStyle = - "bg-[#20C86E] rounded-[30px] text-white font-semibold"; - - const disabledButtonStyle = "bg-form rounded-[30px] text-main font-semibold"; - - const previewMessageUserStyle = - "bg-[#20C86E] rounded-[10px] text-white font-semibold px-[30px] py-[6px]"; - if (props.state === "active") { - return ( - - ); - } else if (props.state === "disabled") { - return ( - - ); - } else if (props.state === "previewMessage") { - return ( - - ); - } -}; - -export default ButtonComponent; diff --git a/client/src/components_v2/ChatHeader.tsx b/client/src/components_v2/ChatHeader.tsx deleted file mode 100644 index 24e514c..0000000 --- a/client/src/components_v2/ChatHeader.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { handleImageError } from "../utils/imageErrorHandler"; -import DevImage from "../assets/dev.png"; - -interface chatHeaderComponentProps { - avatar: string; - name: string; - usersAmount: number; -} -const ChatHeaderComponent = (props: chatHeaderComponentProps) => { - return ( -
    -
  • - -
    -
    -

    - {props.name ?? " название чата "} -

    -

    - {props.usersAmount ?? "0"} участников -

    -
    -
    -
  • -
    - ); -}; - -export default ChatHeaderComponent; diff --git a/client/src/components_v2/ChatMemberCard.tsx b/client/src/components_v2/ChatMemberCard.tsx deleted file mode 100644 index e012da7..0000000 --- a/client/src/components_v2/ChatMemberCard.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { getUserById } from "../api/api"; -import { UserData } from "../types/user.interface"; -import { useNavigate } from "react-router-dom"; -import { initData } from "@telegram-apps/sdk-react"; -import { useQuery } from "@tanstack/react-query"; - -const cardVariants = { - hidden: { opacity: 0, y: 20 }, - visible: (index: number) => ({ - opacity: 1, - y: 0, - transition: { - delay: index > 10 ? index * 0.01 : 11 * 0.01, - duration: 0.4, - ease: "easeOut", - }, - }), -}; - -const ChatMemberCard = ( - props: UserData & { - index: number; - animated?: boolean; - onAnimationComplete?: () => void; - }, -) => { - const navigate = useNavigate(); - const userData = useQuery(`use`, () => - getUserById(initData.raw() ?? "", props.id ?? ""), - ); -}; - -export default ChatMemberCard; From bd904731d23b6b95349d29b79e631007da7568b9 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 1 May 2025 16:36:59 +0300 Subject: [PATCH 08/78] chats information store --- client/src/pages/chat.page.tsx | 12 +++---- client/src/stores/chatSearch.store.ts | 51 ++++++++++++++++++++------- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/client/src/pages/chat.page.tsx b/client/src/pages/chat.page.tsx index dcdcee4..53cb258 100644 --- a/client/src/pages/chat.page.tsx +++ b/client/src/pages/chat.page.tsx @@ -21,19 +21,19 @@ const ChatPage = () => { const { chatId } = useParams(); const [loadedFirstTime, setLoadedFirstTime] = useState(false); - const { searchQuery, setSearchQuery, setScroll, scroll } = + const { getScroll, saveScroll, getSearchQuery, saveSearchQuery } = useChatSearchStore(); const { data: chatData, isPending } = useSearchUsersInChat( chatId ?? "", - searchQuery, + getSearchQuery(chatId ?? ""), ); const { data: previewChatData, isSuccess } = useChatPreview(chatId ?? ""); const scrollContainerRef = useRef(null); const handleScroll = () => { if (scrollContainerRef.current) { - setScroll(scrollContainerRef.current.scrollTop); + saveScroll(chatId ?? "", scrollContainerRef.current.scrollTop); } }; @@ -41,7 +41,7 @@ const ChatPage = () => { const timer = setTimeout(() => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollTo({ - top: scroll, + top: getScroll(chatId ?? ""), behavior: "smooth", }); } @@ -74,8 +74,8 @@ const ChatPage = () => { )} saveSearchQuery(chatId ?? "", value)} placeholder="Поиск участников" className="mt-[20px]" /> diff --git a/client/src/stores/chatSearch.store.ts b/client/src/stores/chatSearch.store.ts index 246224b..8302c86 100644 --- a/client/src/stores/chatSearch.store.ts +++ b/client/src/stores/chatSearch.store.ts @@ -1,21 +1,48 @@ import { create } from "zustand"; -interface ChatSearchState { +export interface UIChatSearchData { searchQuery: string; - chatID: string; scroll: number; - setSearchQuery: (query: string) => void; - setChatID: (chatId: string) => void; - setScroll: (scroll: number) => void; } -const useChatSearchStore = create((set) => ({ - searchQuery: "", - chatID: "", - scroll: 0, - setScroll: (scroll: number) => set(() => ({ scroll: scroll })), - setSearchQuery: (query: string) => set(() => ({ searchQuery: query })), - setChatID: (chatID: string) => set(() => ({ chatID: chatID })), +interface ChatsDataStore { + chats: Map; + + saveChatData: (chatId: string, data: Partial) => void; + saveSearchQuery: (chatId: string, query: string) => void; + saveScroll: (chatId: string, scroll: number) => void; + + getScroll: (chatId: string) => number; + getSearchQuery: (chatId: string) => string; +} + +const useChatSearchStore = create((set, get) => ({ + chats: new Map(), + + saveChatData: (chatId, data) => + set((state) => { + const prev = state.chats.get(chatId) || { searchQuery: "", scroll: 0 }; + const updated = { ...prev, ...data }; + const newMap = new Map(state.chats); + newMap.set(chatId, updated); + return { chats: newMap }; + }), + + saveSearchQuery: (chatId, query) => { + get().saveChatData(chatId, { searchQuery: query }); + }, + + saveScroll: (chatId, scroll) => { + get().saveChatData(chatId, { scroll }); + }, + + getScroll: (chatId) => { + return get().chats.get(chatId)?.scroll ?? 0; + }, + + getSearchQuery: (chatId) => { + return get().chats.get(chatId)?.searchQuery ?? ""; + }, })); export default useChatSearchStore; From a55a67e0fa0d4cba1bc39d6dd9fced25d4f217bb Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 1 May 2025 17:16:39 +0300 Subject: [PATCH 09/78] fix back button behavior --- client/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 55a16fb..3c325d2 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -33,7 +33,7 @@ function App() { useEffect(() => { setBackButtonHandler(); - }, [setBackButtonHandler]); + }, []); return ( From 72e999c0d469b9fdb7f8029d361547df4f7fbbad Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Wed, 7 May 2025 16:33:36 +0300 Subject: [PATCH 10/78] base types --- client/src/pages/initial.page.tsx | 4 +-- .../types/community/community.interface.ts | 13 ++++++++ .../community/communityConfig.interface.ts | 5 +++ client/src/types/fields/field.interface.ts | 11 +++++++ client/src/types/fields/field.type.ts | 1 + .../types/fields/fieldTextArea.interface.ts | 3 ++ .../types/fields/fieldTextInput.interface.ts | 3 ++ .../src/types/fields/fieldValue.interface.ts | 9 ++++++ .../fields/values/fieldValue.interface.ts | 9 ++++++ .../values/fieldValueTextArea.interface.ts | 3 ++ .../values/fieldValutTextInput.interface.ts | 3 ++ client/src/types/member/member.interface.ts | 10 ++++++ .../types/member/memberConfig.interface.ts | 5 +++ .../postApiTypes/createCommunity.interface.ts | 8 +++++ .../postApiTypes/patchCommunity.interface.ts | 10 ++++++ .../types/postApiTypes/patchMe.interface.ts | 5 +++ client/src/types/user.interface.ts | 32 ------------------- client/src/types/user/user.interface.ts | 7 ++++ .../src/types/user/userProfile.interface.ts | 7 ++++ 19 files changed, 114 insertions(+), 34 deletions(-) create mode 100644 client/src/types/community/community.interface.ts create mode 100644 client/src/types/community/communityConfig.interface.ts create mode 100644 client/src/types/fields/field.interface.ts create mode 100644 client/src/types/fields/field.type.ts create mode 100644 client/src/types/fields/fieldTextArea.interface.ts create mode 100644 client/src/types/fields/fieldTextInput.interface.ts create mode 100644 client/src/types/fields/fieldValue.interface.ts create mode 100644 client/src/types/fields/values/fieldValue.interface.ts create mode 100644 client/src/types/fields/values/fieldValueTextArea.interface.ts create mode 100644 client/src/types/fields/values/fieldValutTextInput.interface.ts create mode 100644 client/src/types/member/member.interface.ts create mode 100644 client/src/types/member/memberConfig.interface.ts create mode 100644 client/src/types/postApiTypes/createCommunity.interface.ts create mode 100644 client/src/types/postApiTypes/patchCommunity.interface.ts create mode 100644 client/src/types/postApiTypes/patchMe.interface.ts delete mode 100644 client/src/types/user.interface.ts create mode 100644 client/src/types/user/user.interface.ts create mode 100644 client/src/types/user/userProfile.interface.ts diff --git a/client/src/pages/initial.page.tsx b/client/src/pages/initial.page.tsx index f7bf7b2..629f45b 100644 --- a/client/src/pages/initial.page.tsx +++ b/client/src/pages/initial.page.tsx @@ -3,9 +3,9 @@ import { useNavigate } from "react-router-dom"; import useInitDataStore from "../stores/InitData.store"; const InitialPage = () => { - const { initDataStartParam: startParam } = useInitDataStore(); + const { initDataStartParam: startParam, initData } = useInitDataStore(); const navigate = useNavigate(); - + console.log(initData); useEffect(() => { if ( startParam !== undefined && diff --git a/client/src/types/community/community.interface.ts b/client/src/types/community/community.interface.ts new file mode 100644 index 0000000..f57136c --- /dev/null +++ b/client/src/types/community/community.interface.ts @@ -0,0 +1,13 @@ +import { Member } from "../member/member.interface"; +import { CommunityConfig } from "./communityConfig.interface"; + +export interface Community { + avatar: string; + config: CommunityConfig; + description: string; + id: string; + membersCount: number; + name: string; + tgChatID: number; + members: Member[]; +} diff --git a/client/src/types/community/communityConfig.interface.ts b/client/src/types/community/communityConfig.interface.ts new file mode 100644 index 0000000..6fb4711 --- /dev/null +++ b/client/src/types/community/communityConfig.interface.ts @@ -0,0 +1,5 @@ +import { Field } from "../fields/field.interface"; + +export interface CommunityConfig { + fields: Field[]; +} diff --git a/client/src/types/fields/field.interface.ts b/client/src/types/fields/field.interface.ts new file mode 100644 index 0000000..a6b558b --- /dev/null +++ b/client/src/types/fields/field.interface.ts @@ -0,0 +1,11 @@ +import { FieldType } from "./field.type"; +import { FieldTextArea } from "./fieldTextArea.interface"; +import { FieldTextInput } from "./fieldTextInput.interface"; + +export interface Field { + description: string; + textarea?: FieldTextArea; + textinput?: FieldTextInput; + title: string; + type: FieldType; +} diff --git a/client/src/types/fields/field.type.ts b/client/src/types/fields/field.type.ts new file mode 100644 index 0000000..282427d --- /dev/null +++ b/client/src/types/fields/field.type.ts @@ -0,0 +1 @@ +export type FieldType = "textinput" | "textarea"; diff --git a/client/src/types/fields/fieldTextArea.interface.ts b/client/src/types/fields/fieldTextArea.interface.ts new file mode 100644 index 0000000..3354cc2 --- /dev/null +++ b/client/src/types/fields/fieldTextArea.interface.ts @@ -0,0 +1,3 @@ +export interface FieldTextArea { + default: string; +} diff --git a/client/src/types/fields/fieldTextInput.interface.ts b/client/src/types/fields/fieldTextInput.interface.ts new file mode 100644 index 0000000..5e7d411 --- /dev/null +++ b/client/src/types/fields/fieldTextInput.interface.ts @@ -0,0 +1,3 @@ +export interface FieldTextInput { + default: string; +} diff --git a/client/src/types/fields/fieldValue.interface.ts b/client/src/types/fields/fieldValue.interface.ts new file mode 100644 index 0000000..eca6514 --- /dev/null +++ b/client/src/types/fields/fieldValue.interface.ts @@ -0,0 +1,9 @@ +import { FieldType } from "./field.type"; +import { FieldValueTextArea } from "./values/fieldValueTextArea.interface"; +import { FieldValueTextInput } from "./values/fieldValutTextInput.interface"; + +export interface FieldValue { + textarea?: FieldValueTextArea; + textinput?: FieldValueTextInput; + type: FieldType; +} diff --git a/client/src/types/fields/values/fieldValue.interface.ts b/client/src/types/fields/values/fieldValue.interface.ts new file mode 100644 index 0000000..43670b5 --- /dev/null +++ b/client/src/types/fields/values/fieldValue.interface.ts @@ -0,0 +1,9 @@ +import { FieldType } from "../field.type"; +import { FieldValueTextArea } from "./fieldValueTextArea.interface"; +import { FieldValueTextInput } from "./fieldValutTextInput.interface"; + +export interface FieldValue { + textarea?: FieldValueTextArea; + textinput?: FieldValueTextInput; + type: FieldType; +} diff --git a/client/src/types/fields/values/fieldValueTextArea.interface.ts b/client/src/types/fields/values/fieldValueTextArea.interface.ts new file mode 100644 index 0000000..8dc34f4 --- /dev/null +++ b/client/src/types/fields/values/fieldValueTextArea.interface.ts @@ -0,0 +1,3 @@ +export interface FieldValueTextArea { + value: string; +} diff --git a/client/src/types/fields/values/fieldValutTextInput.interface.ts b/client/src/types/fields/values/fieldValutTextInput.interface.ts new file mode 100644 index 0000000..48e9c3b --- /dev/null +++ b/client/src/types/fields/values/fieldValutTextInput.interface.ts @@ -0,0 +1,3 @@ +export interface FieldValueTextInput { + value: string; +} diff --git a/client/src/types/member/member.interface.ts b/client/src/types/member/member.interface.ts new file mode 100644 index 0000000..af96cb6 --- /dev/null +++ b/client/src/types/member/member.interface.ts @@ -0,0 +1,10 @@ +import { Community } from "../community/community.interface"; +import { User } from "../user/user.interface"; +import { MemberConfig } from "./memberConfig.interface"; + +export interface Member { + community: Community; + config: MemberConfig; + isAdmin: boolean; + user: User; +} diff --git a/client/src/types/member/memberConfig.interface.ts b/client/src/types/member/memberConfig.interface.ts new file mode 100644 index 0000000..c09292b --- /dev/null +++ b/client/src/types/member/memberConfig.interface.ts @@ -0,0 +1,5 @@ +import { FieldValue } from "../fields/values/fieldValue.interface"; + +export interface MemberConfig { + fields: Record; +} diff --git a/client/src/types/postApiTypes/createCommunity.interface.ts b/client/src/types/postApiTypes/createCommunity.interface.ts new file mode 100644 index 0000000..afc4323 --- /dev/null +++ b/client/src/types/postApiTypes/createCommunity.interface.ts @@ -0,0 +1,8 @@ +import { CommunityConfig } from "../community/communityConfig.interface"; + +export interface CreateCommunity { + avatar: string; + config: CommunityConfig; + description: string; + name: string; +} diff --git a/client/src/types/postApiTypes/patchCommunity.interface.ts b/client/src/types/postApiTypes/patchCommunity.interface.ts new file mode 100644 index 0000000..cc9e096 --- /dev/null +++ b/client/src/types/postApiTypes/patchCommunity.interface.ts @@ -0,0 +1,10 @@ +import { CommunityConfig } from "../community/communityConfig.interface"; + +export interface PatchCommunity { + avatar: string; + config: CommunityConfig; + description: string; + id: string; + name: string; + tgChatID: number; +} diff --git a/client/src/types/postApiTypes/patchMe.interface.ts b/client/src/types/postApiTypes/patchMe.interface.ts new file mode 100644 index 0000000..37c4117 --- /dev/null +++ b/client/src/types/postApiTypes/patchMe.interface.ts @@ -0,0 +1,5 @@ +export interface PatchMe { + firstName: string; + id: string; + lastName: string; +} diff --git a/client/src/types/user.interface.ts b/client/src/types/user.interface.ts deleted file mode 100644 index 0cf1756..0000000 --- a/client/src/types/user.interface.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface ProfileUserData { - firstName: string | null; - lastName: string | null; - company: string | null; - role: string | null; - avatar: string | null; - telegramUsername: string | null; - bio: string | null; -} - -export interface UserData extends ProfileUserData { - id: string | null; // backend ID - telegramId: string | null; - isRegistered: boolean; -} - -export interface ChatData { - id: string; // backend ID - telegramId: string; - name: string | null; - avatar: string | null; - users: UserData[]; -} - -export interface ChatPreviewData { - avatar: string | null; - usersAmount: string | null; - name: string | null; - id: string | null; - telegramId: string | null; - isMember: boolean | null; -} diff --git a/client/src/types/user/user.interface.ts b/client/src/types/user/user.interface.ts new file mode 100644 index 0000000..61567fd --- /dev/null +++ b/client/src/types/user/user.interface.ts @@ -0,0 +1,7 @@ +export interface User { + avatar: string; + firstName: string; + id: string; + telegramId: number; + telegramUsername: string; +} diff --git a/client/src/types/user/userProfile.interface.ts b/client/src/types/user/userProfile.interface.ts new file mode 100644 index 0000000..9d05890 --- /dev/null +++ b/client/src/types/user/userProfile.interface.ts @@ -0,0 +1,7 @@ +import { Member } from "../member/member.interface"; +import { User } from "./user.interface"; + +export interface UserProfile { + members: Member[]; + user: User; +} From 1a92d2f39f5f4cfca0401d133791770732e21448 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Wed, 7 May 2025 18:41:20 +0300 Subject: [PATCH 11/78] api implementation --- client/src/api/api.ts | 214 ------------------------------ client/src/api/communities.api.ts | 173 ++++++++++++++++++++++++ client/src/api/users.api.ts | 59 ++++++++ 3 files changed, 232 insertions(+), 214 deletions(-) create mode 100644 client/src/api/communities.api.ts create mode 100644 client/src/api/users.api.ts diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 46663c4..153cbfd 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -1,6 +1,5 @@ import axios from "axios"; import { API_URL } from "../shared/constants"; -import { UserData, ChatData, ChatPreviewData } from "../types/user.interface"; export const api = axios.create({ baseURL: `${API_URL}/api/v1`, headers: { @@ -8,216 +7,3 @@ export const api = axios.create({ }, maxRedirects: 0, }); - -export const getChat = async ( - initData: string, - chatId: string, -): Promise => { - try { - const response = await api.get(`/chats/id/${chatId}`, { - headers: { "X-Api-Token": initData }, - }); - return response.data; - } catch (error) { - console.error("Ошибка при получении чата:", error); - return null; - } -}; - -export const getChatPreview = async ( - initData: string, - chatId: string, -): Promise => { - try { - const response = await api.get( - `/chats/id/${chatId}/preview`, - { - headers: { "X-Api-Token": initData }, - }, - ); - return response.data; - } catch (error) { - console.error("Ошибка при получении чата:", error); - return null; - } -}; - -export const getChats = async ( - initData: string, -): Promise => { - try { - const response = await api.get(`/chats`, { - headers: { "X-Api-Token": initData }, - }); - return response.data; - } catch (error) { - console.error("Ошибка при получении чатов:", error); - return null; - } -}; - -// userID – inner backend id -export const getUserById = async ( - initData: string, - userId: string, -): Promise => { - try { - const response = await api.get(`/users/id/${userId}`, { - headers: { "X-Api-Token": initData }, - }); - return response.data; - } catch (error) { - console.error("Ошибка при получении профиля текущего пользователя:", error); - return null; - } -}; - -export const getMe = async (initData: string): Promise => { - try { - const response = await api.get(`/users/me`, { - headers: { "X-Api-Token": initData }, - }); - return response.data; - } catch (error) { - console.error("Ошибка при получении профиля текущего пользователя:", error); - return null; - } -}; - -export const postMe = async ( - initData: string, - newData: Pick< - UserData, - "firstName" | "lastName" | "bio" | "role" | "company" - >, -): Promise => { - try { - const response = await api.put(`/users/me`, newData, { - headers: { "X-Api-Token": initData }, - }); - return response.data; - } catch (error) { - console.error("Ошибка при изменении профиля", error); - return null; - } -}; - -export const createMe = async ( - initData: string, - userData: Pick< - UserData, - "firstName" | "lastName" | "bio" | "role" | "company" - >, -): Promise | null> => { - try { - const response = await api.post("/users/me", userData, { - headers: { - "X-Api-Token": initData, - }, - }); - return response.data; - } catch (error) { - console.error("Ошибка при содании профиля", error); - } - return null; -}; - -export const joinMe = async ( - initData: string, - chatId: string, -): Promise => { - try { - const response = await api.post( - `/chats/id/${chatId}/join`, - {}, - { - headers: { - "X-Api-Token": initData, - }, - }, - ); - return response.status === 200; - } catch (error) { - console.error("ошибка при присоединении к чату", error); - return false; - } -}; - -export const searchInChat = async ( - initData: string, - chatId: string, - query: string, -): Promise => { - try { - const response = await api.get(`/chats/id/${chatId}/search`, { - params: { - q: query, - }, - headers: { - "X-Api-Token": initData, - }, - }); - return response.data; - } catch (error) { - console.error("Ошибка при поиске пользователей:", error); - return null; - } -}; - -export const searchChats = async ( - initData: string, - query: string, -): Promise => { - try { - const response = await api.get(`/chats/search`, { - params: { - q: query, - }, - headers: { - "X-Api-Token": initData, - }, - }); - return response.data; - } catch (error) { - console.error("Ошибка при поиске пользователей:", error); - return null; - } -}; - -export const leaveChat = async ( - initData: string, - chatId: string, -): Promise => { - try { - const response = await api.post( - `/chats/id/${chatId}/leave`, - {}, - { - headers: { - "X-Api-Token": initData, - }, - }, - ); - return response.status === 200; - } catch (error) { - console.error(error); - return false; - } -}; - -export const getMePreview = async ( - initData: string, -): Promise => { - try { - const response = await api.get(`/users/me/preview`, { - headers: { "X-Api-Token": initData }, - }); - return response.data; - } catch (error) { - console.error("Ошибка при получении профиля текущего пользователя:", error); - return null; - } -}; diff --git a/client/src/api/communities.api.ts b/client/src/api/communities.api.ts new file mode 100644 index 0000000..a7b98d6 --- /dev/null +++ b/client/src/api/communities.api.ts @@ -0,0 +1,173 @@ +import { Community } from "../types/community/community.interface"; +import { Member } from "../types/member/member.interface"; +import { MemberConfig } from "../types/member/memberConfig.interface"; +import { CreateCommunity } from "../types/postApiTypes/createCommunity.interface"; +import { PatchCommunity } from "../types/postApiTypes/patchCommunity.interface"; +import { api } from "./api"; + +export const getCommunityPreviews = async ( + initData: string, +): Promise => { + try { + const response = await api.get(`/communities`, { + headers: { "X-Api-Token": initData }, + }); + return response.data; + } catch (error) { + console.error("Ошибка при получении сообществ:", error); + return null; + } +}; + +export const createCommunity = async ( + initData: string, + communityData: CreateCommunity, +): Promise => { + try { + const response = await api.post(`/communities`, communityData, { + headers: { "X-Api-Token": initData }, + }); + return response.data; + } catch (error) { + console.error("Ошибка при создании сообщества:", error); + return null; + } +}; + +export const patchCommunity = async ( + initData: string, + patchCommunityData: PatchCommunity, +): Promise => { + try { + const response = await api.patch( + `/communities/id/${patchCommunityData.id}`, + patchCommunityData, + { + headers: { "X-Api-Token": initData }, + }, + ); + return response.data; + } catch (error) { + console.error("Ошибка при патче сообщества:", error); + return null; + } +}; + +export const getCommunity = async ( + initData: string, + id: string, +): Promise => { + try { + const response = await api.get(`/communities/id/${id}`, { + headers: { "X-Api-Token": initData }, + }); + return response.data; + } catch (error) { + console.error("Ошибка при получении сообщества:", error); + return null; + } +}; + +export const joinCommunity = async ( + initData: string, + id: string, +): Promise => { + try { + const response = await api.post(`/communities/id/${id}`, { + headers: { "X-Api-Token": initData }, + }); + return response.data; + } catch (error) { + console.error("Ошибка при присоединении к сообществу:", error); + return null; + } +}; + +export const leaveCommunity = async ( + initData: string, + id: string, +): Promise => { + try { + const response = await api.post(`/communities/id/${id}`, { + headers: { "X-Api-Token": initData }, + }); + return response.data; + } catch (error) { + console.error("Ошибка при выходе из сообщества:", error); + return null; + } +}; + +export const searchMembers = async ( + initData: string, + id: string, + query: string, +): Promise => { + try { + const response = await api.get( + `/communities/id/${id}/members/search`, + { + params: { + q: query, + }, + headers: { "X-Api-Token": initData }, + }, + ); + return response.data; + } catch (error) { + console.error("Ошибка при поиске пользователей", error); + return null; + } +}; + +export const getMember = async ( + initData: string, + communityId: string, + memberId: string, +): Promise => { + try { + const response = await api.get( + `/communities/id/${communityId}/members/${memberId}`, + { + headers: { "X-Api-Token": initData }, + }, + ); + return response.data; + } catch (error) { + console.error("Ошибка при получении пользователя:", error); + return null; + } +}; + +export const getCommunityPreview = async ( + initData: string, + id: string, +): Promise => { + try { + const response = await api.get(`/communities/id/${id}/preview`, { + headers: { "X-Api-Token": initData }, + }); + return response.data; + } catch (error) { + console.error("Ошибка при получении пользователя:", error); + return null; + } +}; + +export const searchCommunities = async ( + initData: string, + query: string, +): Promise => { + try { + const response = await api.get(`/communities/search`, { + headers: { "X-Api-Token": initData }, + params: { + q: query, + }, + }); + return response.data; + } catch (error) { + console.error("Ошибка при поиске сообществ:", error); + return null; + } +}; diff --git a/client/src/api/users.api.ts b/client/src/api/users.api.ts new file mode 100644 index 0000000..ad7558a --- /dev/null +++ b/client/src/api/users.api.ts @@ -0,0 +1,59 @@ +import { PatchMe } from "../types/postApiTypes/patchMe.interface"; +import { User } from "../types/user/user.interface"; +import { UserProfile } from "../types/user/userProfile.interface"; +import { api } from "./api"; + +export const getMe = async (initData: string): Promise => { + try { + const response = await api.get(`/users/me`, { + headers: { "X-Api-Token": initData }, + }); + return response.data; + } catch (error) { + console.error("Ошибка при получении профиля текущего пользователя:", error); + return null; + } +}; + +export const updateMe = async ( + initData: string, + newData: PatchMe, +): Promise => { + try { + const response = await api.put(`/users/me`, newData, { + headers: { "X-Api-Token": initData }, + }); + return response.data; + } catch (error) { + console.error("Ошибка при изменении профиля", error); + return null; + } +}; + +export const createMe = async (initData: string): Promise => { + try { + const response = await api.post("/users/me", { + headers: { + "X-Api-Token": initData, + }, + }); + return response.data; + } catch (error) { + console.error("Ошибка при содании профиля", error); + } + return null; +}; + +export const deleteMe = async (initData: string): Promise => { + try { + const response = await api.delete("/users/me", { + headers: { + "X-Api-Token": initData, + }, + }); + return response.data; + } catch (error) { + console.error("Ошибка при удалении профиля", error); + } + return null; +}; From 0ad7b4edb21e63c3ba51b90c6834d67e071e0573 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Wed, 7 May 2025 22:01:24 +0300 Subject: [PATCH 12/78] new hooks, api types fix --- client/src/api/communities.api.ts | 4 +-- .../fetchHooks/useCommunitiesAll.ts | 26 +++++++++++++++++++ .../fetchHooks/useCommunityPreview.ts | 13 ++++++++++ .../fetchHooks/use\320\241ommunity.ts" | 13 ++++++++++ .../search/useCommunitiesSearch.ts | 15 +++++++++++ client/src/hooks/useChat.ts | 13 ---------- client/src/hooks/useUpdateMe.ts | 11 ++++++++ client/src/hooks/useUser.ts | 13 ---------- 8 files changed, 80 insertions(+), 28 deletions(-) create mode 100644 client/src/hooks/communities/fetchHooks/useCommunitiesAll.ts create mode 100644 client/src/hooks/communities/fetchHooks/useCommunityPreview.ts create mode 100644 "client/src/hooks/communities/fetchHooks/use\320\241ommunity.ts" create mode 100644 client/src/hooks/communities/search/useCommunitiesSearch.ts delete mode 100644 client/src/hooks/useChat.ts delete mode 100644 client/src/hooks/useUser.ts diff --git a/client/src/api/communities.api.ts b/client/src/api/communities.api.ts index a7b98d6..482a92e 100644 --- a/client/src/api/communities.api.ts +++ b/client/src/api/communities.api.ts @@ -142,9 +142,9 @@ export const getMember = async ( export const getCommunityPreview = async ( initData: string, id: string, -): Promise => { +): Promise => { try { - const response = await api.get(`/communities/id/${id}/preview`, { + const response = await api.get(`/communities/id/${id}/preview`, { headers: { "X-Api-Token": initData }, }); return response.data; diff --git a/client/src/hooks/communities/fetchHooks/useCommunitiesAll.ts b/client/src/hooks/communities/fetchHooks/useCommunitiesAll.ts new file mode 100644 index 0000000..fcfb4d1 --- /dev/null +++ b/client/src/hooks/communities/fetchHooks/useCommunitiesAll.ts @@ -0,0 +1,26 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import useInitDataStore from "../../../stores/InitData.store"; +import { getCommunityPreviews } from "../../../api/communities.api"; +import { Community } from "../../../types/community/community.interface"; + +const useCommunititesAll = () => { + const { initData } = useInitDataStore(); + const queryClient = useQueryClient(); + return useQuery({ + queryKey: [`communities/`, initData], + queryFn: async () => { + const communities = await getCommunityPreviews(initData); + + communities?.forEach((community: Community) => { + queryClient.setQueryData( + [`communities/${community.id}`, initData, community.id], + community, + ); + }); + + return communities; + }, + }); +}; + +export default useCommunititesAll; diff --git a/client/src/hooks/communities/fetchHooks/useCommunityPreview.ts b/client/src/hooks/communities/fetchHooks/useCommunityPreview.ts new file mode 100644 index 0000000..949081b --- /dev/null +++ b/client/src/hooks/communities/fetchHooks/useCommunityPreview.ts @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; +import useInitDataStore from "../../../stores/InitData.store"; +import { getCommunityPreview } from "../../../api/communities.api"; + +const useCommunityPreview = (id: string) => { + const { initData } = useInitDataStore(); + return useQuery({ + queryKey: [`communities/${id}`, initData, id], + queryFn: () => getCommunityPreview(initData, id), + }); +}; + +export default useCommunityPreview; diff --git "a/client/src/hooks/communities/fetchHooks/use\320\241ommunity.ts" "b/client/src/hooks/communities/fetchHooks/use\320\241ommunity.ts" new file mode 100644 index 0000000..cc5d62b --- /dev/null +++ "b/client/src/hooks/communities/fetchHooks/use\320\241ommunity.ts" @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; +import useInitDataStore from "../stores/InitData.store"; +import { getCommunity } from "../api/communities.api"; + +const useCommunity = (id: string) => { + const { initData } = useInitDataStore(); + return useQuery({ + queryKey: [`communities/${id}`, initData, id], + queryFn: () => getCommunity(initData, id), + }); +}; + +export default useCommunity; diff --git a/client/src/hooks/communities/search/useCommunitiesSearch.ts b/client/src/hooks/communities/search/useCommunitiesSearch.ts new file mode 100644 index 0000000..2c97f19 --- /dev/null +++ b/client/src/hooks/communities/search/useCommunitiesSearch.ts @@ -0,0 +1,15 @@ +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import useInitDataStore from "../../../stores/InitData.store"; +import { searchCommunities } from "../../../api/communities.api"; + +const useCommunitiesSearch = (query: string) => { + const { initData } = useInitDataStore(); + + return useQuery({ + queryKey: ["/communities", initData, query], + queryFn: () => searchCommunities(initData, query), + placeholderData: keepPreviousData, + }); +}; + +export default useCommunitiesSearch; diff --git a/client/src/hooks/useChat.ts b/client/src/hooks/useChat.ts deleted file mode 100644 index 3c9d4e2..0000000 --- a/client/src/hooks/useChat.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getChat } from "../api/api"; -import useInitDataStore from "../stores/InitData.store"; - -const useChat = (chatId: string) => { - const { initData } = useInitDataStore(); - return useQuery({ - queryKey: [`chat/${chatId}`, initData, chatId], - queryFn: () => getChat(initData, chatId), - }); -}; - -export default useChat; diff --git a/client/src/hooks/useUpdateMe.ts b/client/src/hooks/useUpdateMe.ts index 930209d..01ec473 100644 --- a/client/src/hooks/useUpdateMe.ts +++ b/client/src/hooks/useUpdateMe.ts @@ -2,6 +2,17 @@ import { useQueryClient, useMutation } from "@tanstack/react-query"; import { postMe } from "../api/api"; import useInitDataStore from "../stores/InitData.store"; import { UserData } from "../types/user.interface"; +import { PatchMe } from "../types/postApiTypes/patchMe.interface"; +import { updateMe } from "../api/users.api"; + +const useUpdateMe = () => { + const { initData } = useInitDataStore(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (patchData: PatchMe) => updateMe(initData, patchData), + }); +}; const useUpdateMe = () => { const { initData } = useInitDataStore(); diff --git a/client/src/hooks/useUser.ts b/client/src/hooks/useUser.ts deleted file mode 100644 index f0d7ffc..0000000 --- a/client/src/hooks/useUser.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getUserById } from "../api/api"; -import useInitDataStore from "../stores/InitData.store"; - -const useUser = (userId: string) => { - const { initData } = useInitDataStore(); - return useQuery({ - queryKey: [`user/${userId}`, initData, userId], - queryFn: () => getUserById(initData, userId), - }); -}; - -export default useUser; From 63d2b9d43fe25b82485343054c06f87a3b1274a6 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Wed, 7 May 2025 22:18:18 +0300 Subject: [PATCH 13/78] users apu hooks --- .../fetchHooks/use\320\241ommunity.ts" | 4 ++-- client/src/hooks/users/fetchHooks/useGetMe.ts | 14 ++++++++++++++ .../src/hooks/users/mutations/useCreateMe.ts | 18 ++++++++++++++++++ .../src/hooks/users/mutations/useDeleteMe.ts | 18 ++++++++++++++++++ .../src/hooks/users/mutations/useUpdateMe.ts | 19 +++++++++++++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 client/src/hooks/users/fetchHooks/useGetMe.ts create mode 100644 client/src/hooks/users/mutations/useCreateMe.ts create mode 100644 client/src/hooks/users/mutations/useDeleteMe.ts create mode 100644 client/src/hooks/users/mutations/useUpdateMe.ts diff --git "a/client/src/hooks/communities/fetchHooks/use\320\241ommunity.ts" "b/client/src/hooks/communities/fetchHooks/use\320\241ommunity.ts" index cc5d62b..32848b9 100644 --- "a/client/src/hooks/communities/fetchHooks/use\320\241ommunity.ts" +++ "b/client/src/hooks/communities/fetchHooks/use\320\241ommunity.ts" @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; -import useInitDataStore from "../stores/InitData.store"; -import { getCommunity } from "../api/communities.api"; +import useInitDataStore from "../../../stores/InitData.store"; +import { getCommunity } from "../../../api/communities.api"; const useCommunity = (id: string) => { const { initData } = useInitDataStore(); diff --git a/client/src/hooks/users/fetchHooks/useGetMe.ts b/client/src/hooks/users/fetchHooks/useGetMe.ts new file mode 100644 index 0000000..011e1e1 --- /dev/null +++ b/client/src/hooks/users/fetchHooks/useGetMe.ts @@ -0,0 +1,14 @@ +import { useQuery } from "@tanstack/react-query"; +import useInitDataStore from "../../../stores/InitData.store"; +import { getMe } from "../../../api/users.api"; + +const useGetMe = () => { + const { initData } = useInitDataStore(); + + return useQuery({ + queryFn: () => getMe(initData), + queryKey: [`users/me`, initData], + }); +}; + +export default useGetMe; diff --git a/client/src/hooks/users/mutations/useCreateMe.ts b/client/src/hooks/users/mutations/useCreateMe.ts new file mode 100644 index 0000000..0018c52 --- /dev/null +++ b/client/src/hooks/users/mutations/useCreateMe.ts @@ -0,0 +1,18 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import useInitDataStore from "../../../stores/InitData.store"; +import { createMe } from "../../../api/users.api"; + +const useCreateMe = () => { + const { initData } = useInitDataStore(); + + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => createMe(initData), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users/me", initData] }); + }, + }); +}; + +export default useCreateMe; diff --git a/client/src/hooks/users/mutations/useDeleteMe.ts b/client/src/hooks/users/mutations/useDeleteMe.ts new file mode 100644 index 0000000..a223601 --- /dev/null +++ b/client/src/hooks/users/mutations/useDeleteMe.ts @@ -0,0 +1,18 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import useInitDataStore from "../../../stores/InitData.store"; +import { deleteMe } from "../../../api/users.api"; + +const useDeleteMe = () => { + const { initData } = useInitDataStore(); + + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => deleteMe(initData), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users/me", initData] }); + }, + }); +}; + +export default useDeleteMe; diff --git a/client/src/hooks/users/mutations/useUpdateMe.ts b/client/src/hooks/users/mutations/useUpdateMe.ts new file mode 100644 index 0000000..36d9aed --- /dev/null +++ b/client/src/hooks/users/mutations/useUpdateMe.ts @@ -0,0 +1,19 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import useInitDataStore from "../../../stores/InitData.store"; +import { PatchMe } from "../../../types/postApiTypes/patchMe.interface"; +import { updateMe } from "../../../api/users.api"; + +const useUpdateMe = () => { + const { initData } = useInitDataStore(); + + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (newData: PatchMe) => updateMe(initData, newData), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users/me", initData] }); + }, + }); +}; + +export default useUpdateMe; From 4c4c027d756849893b6058e5c3037b35dd59f4de Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Wed, 7 May 2025 22:31:14 +0300 Subject: [PATCH 14/78] cache fixes --- .../fetchHooks/useCommunitiesAll.ts | 4 ++-- .../fetchHooks/useCommunityPreview.ts | 2 +- .../fetchHooks/use\320\241ommunity.ts" | 2 +- .../src/hooks/members/fetchHooks/useMember.ts | 18 ++++++++++++++++++ .../hooks/members/search/useSearchMembers.ts | 19 +++++++++++++++++++ 5 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 client/src/hooks/members/fetchHooks/useMember.ts create mode 100644 client/src/hooks/members/search/useSearchMembers.ts diff --git a/client/src/hooks/communities/fetchHooks/useCommunitiesAll.ts b/client/src/hooks/communities/fetchHooks/useCommunitiesAll.ts index fcfb4d1..3ac011f 100644 --- a/client/src/hooks/communities/fetchHooks/useCommunitiesAll.ts +++ b/client/src/hooks/communities/fetchHooks/useCommunitiesAll.ts @@ -7,13 +7,13 @@ const useCommunititesAll = () => { const { initData } = useInitDataStore(); const queryClient = useQueryClient(); return useQuery({ - queryKey: [`communities/`, initData], + queryKey: [`/communities`, initData], queryFn: async () => { const communities = await getCommunityPreviews(initData); communities?.forEach((community: Community) => { queryClient.setQueryData( - [`communities/${community.id}`, initData, community.id], + [`/communities/${community.id}`, initData, community.id], community, ); }); diff --git a/client/src/hooks/communities/fetchHooks/useCommunityPreview.ts b/client/src/hooks/communities/fetchHooks/useCommunityPreview.ts index 949081b..f85afbf 100644 --- a/client/src/hooks/communities/fetchHooks/useCommunityPreview.ts +++ b/client/src/hooks/communities/fetchHooks/useCommunityPreview.ts @@ -5,7 +5,7 @@ import { getCommunityPreview } from "../../../api/communities.api"; const useCommunityPreview = (id: string) => { const { initData } = useInitDataStore(); return useQuery({ - queryKey: [`communities/${id}`, initData, id], + queryKey: [`/communities/${id}`, initData], queryFn: () => getCommunityPreview(initData, id), }); }; diff --git "a/client/src/hooks/communities/fetchHooks/use\320\241ommunity.ts" "b/client/src/hooks/communities/fetchHooks/use\320\241ommunity.ts" index 32848b9..9b1069b 100644 --- "a/client/src/hooks/communities/fetchHooks/use\320\241ommunity.ts" +++ "b/client/src/hooks/communities/fetchHooks/use\320\241ommunity.ts" @@ -5,7 +5,7 @@ import { getCommunity } from "../../../api/communities.api"; const useCommunity = (id: string) => { const { initData } = useInitDataStore(); return useQuery({ - queryKey: [`communities/${id}`, initData, id], + queryKey: [`/communities/${id}`, initData], queryFn: () => getCommunity(initData, id), }); }; diff --git a/client/src/hooks/members/fetchHooks/useMember.ts b/client/src/hooks/members/fetchHooks/useMember.ts new file mode 100644 index 0000000..f71138b --- /dev/null +++ b/client/src/hooks/members/fetchHooks/useMember.ts @@ -0,0 +1,18 @@ +import { useQuery } from "@tanstack/react-query"; +import useInitDataStore from "../../../stores/InitData.store"; +import { getMember } from "../../../api/communities.api"; + +const useMember = (communityId: string, memberId: string) => { + const { initData } = useInitDataStore(); + return useQuery({ + queryFn: () => getMember(initData, communityId, memberId), + queryKey: [ + `/communities/${communityId}/member/${memberId}`, + initData, + communityId, + memberId, + ], + }); +}; + +export default useMember; diff --git a/client/src/hooks/members/search/useSearchMembers.ts b/client/src/hooks/members/search/useSearchMembers.ts new file mode 100644 index 0000000..c286fc4 --- /dev/null +++ b/client/src/hooks/members/search/useSearchMembers.ts @@ -0,0 +1,19 @@ +import { useQuery } from "@tanstack/react-query"; +import useInitDataStore from "../../../stores/InitData.store"; +import { searchMembers } from "../../../api/communities.api"; + +const useSearchMembers = (communityId: string, query: string) => { + const { initData } = useInitDataStore(); + + return useQuery({ + queryFn: () => searchMembers(initData, communityId, query), + queryKey: [ + `/communities/${communityId}/members`, + initData, + communityId, + query, + ], + }); +}; + +export default useSearchMembers; From cdf94f4fe23d4234510d9177b6b68c3710e2833d Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Wed, 7 May 2025 23:04:16 +0300 Subject: [PATCH 15/78] join and leave community hooks --- .../communities/mutations/useJoinCommunity.ts | 21 ++++++++++++ .../mutations/useLeaveCommunity.ts | 33 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 client/src/hooks/communities/mutations/useJoinCommunity.ts create mode 100644 client/src/hooks/communities/mutations/useLeaveCommunity.ts diff --git a/client/src/hooks/communities/mutations/useJoinCommunity.ts b/client/src/hooks/communities/mutations/useJoinCommunity.ts new file mode 100644 index 0000000..e936c13 --- /dev/null +++ b/client/src/hooks/communities/mutations/useJoinCommunity.ts @@ -0,0 +1,21 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import useInitDataStore from "../../../stores/InitData.store"; +import { joinCommunity } from "../../../api/communities.api"; + +const useJoinCommunity = () => { + const { initData } = useInitDataStore(); + + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (communityId: string) => joinCommunity(initData, communityId), + onSuccess: (_, communityId) => { + queryClient.invalidateQueries({ queryKey: ["/communities", initData] }); + queryClient.invalidateQueries({ + queryKey: [`/communities/${communityId}`, initData], + }); + }, + }); +}; + +export default useJoinCommunity; diff --git a/client/src/hooks/communities/mutations/useLeaveCommunity.ts b/client/src/hooks/communities/mutations/useLeaveCommunity.ts new file mode 100644 index 0000000..6af3ce2 --- /dev/null +++ b/client/src/hooks/communities/mutations/useLeaveCommunity.ts @@ -0,0 +1,33 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import useInitDataStore from "../../../stores/InitData.store"; +import { leaveCommunity } from "../../../api/communities.api"; +import { Community } from "../../../types/community/community.interface"; + +const useLeaveCommunity = () => { + const { initData } = useInitDataStore(); + + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (communityId: string) => leaveCommunity(initData, communityId), + onSuccess: (_, communityId) => { + queryClient.setQueryData( + ["/communities", initData], + (old) => old?.filter((community) => community.id !== communityId) ?? [], + ); + + queryClient.setQueriesData( + { queryKey: ["/communities", initData] }, + (old: Community[] | undefined) => + old?.filter((community) => community.id !== communityId) ?? [], + ); + + queryClient.setQueryData( + [`/communities/${communityId}`, initData, communityId], + null, + ); + }, + }); +}; + +export default useLeaveCommunity; From 48c741767e985df92dfb5ac61c9d2996c2334a20 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 8 May 2025 00:29:36 +0300 Subject: [PATCH 16/78] used fetching community api, some minor fixes in api types --- client/src/App.tsx | 28 +- client/src/api/communities.api.ts | 30 +- .../src/components/chatPreview.component.tsx | 11 +- .../requireMembership.component.tsx | 8 +- client/src/hooks/useChatPreview.ts | 13 - client/src/hooks/useChats.ts | 13 - client/src/hooks/useCreateMe.ts | 27 -- client/src/hooks/useJoinMe.ts | 26 -- client/src/hooks/useLeaveChat.ts | 21 - client/src/hooks/useMe.ts | 13 - client/src/hooks/useMePreview.ts | 13 - client/src/hooks/useSearchChats.ts | 15 - client/src/hooks/useSearchUsers.ts | 15 - client/src/hooks/useUpdateMe.ts | 38 -- .../{chats.page.tsx => communities.page.tsx} | 20 +- .../{chat.page.tsx => community.page.tsx} | 18 +- client/src/pages/currentProfile.page.tsx | 21 +- client/src/pages/editProfile.page.tsx | 423 +++++++++--------- client/src/pages/initial.page.tsx | 4 +- client/src/pages/invitation.page.tsx | 107 ++--- client/src/pages/profile.page.tsx | 23 +- client/src/pages/registration.page.tsx | 378 ++++++++-------- client/src/types/user/user.interface.ts | 1 + .../protectedRoute.tsx} | 7 +- 24 files changed, 536 insertions(+), 737 deletions(-) delete mode 100644 client/src/hooks/useChatPreview.ts delete mode 100644 client/src/hooks/useChats.ts delete mode 100644 client/src/hooks/useCreateMe.ts delete mode 100644 client/src/hooks/useJoinMe.ts delete mode 100644 client/src/hooks/useLeaveChat.ts delete mode 100644 client/src/hooks/useMe.ts delete mode 100644 client/src/hooks/useMePreview.ts delete mode 100644 client/src/hooks/useSearchChats.ts delete mode 100644 client/src/hooks/useSearchUsers.ts delete mode 100644 client/src/hooks/useUpdateMe.ts rename client/src/pages/{chats.page.tsx => communities.page.tsx} (91%) rename client/src/pages/{chat.page.tsx => community.page.tsx} (89%) rename client/src/{components/protectedRoute.component.tsx => utils/protectedRoute.tsx} (64%) diff --git a/client/src/App.tsx b/client/src/App.tsx index 3c325d2..eee7a68 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,19 +1,16 @@ import "./App.css"; import { Route, Routes, useLocation, useNavigate } from "react-router-dom"; -import { AboutFirstPage, AboutSecondPage } from "./pages/about.page"; -import ChatPage from "./pages/chat.page"; -import ChatsPage from "./pages/chats.page"; import { useCallback, useEffect } from "react"; import { backButton } from "@telegram-apps/sdk-react"; -import ProtectedRoute from "./components/protectedRoute.component"; -import CurrentProfilePage from "./pages/currentProfile.page"; -import EditProfilePage from "./pages/editProfile.page"; -import RegistrationPage from "./pages/registration.page"; import InitialPage from "./pages/initial.page"; -import InvitationPage from "./pages/invitation.page"; -import ProfilePage from "./pages/profile.page"; import { AnimatePresence, motion } from "framer-motion"; import InitDataWrapper from "./utils/InitDataWrapper"; +import ProtectedRoute from "./utils/protectedRoute"; +import { AboutFirstPage, AboutSecondPage } from "./pages/about.page"; +import ChatPage from "./pages/community.page"; +import CurrentProfilePage from "./pages/currentProfile.page"; +import ProfilePage from "./pages/profile.page"; +import CommunitiesPage from "./pages/communities.page"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -51,17 +48,20 @@ function App() { > }> - } /> + } /> } /> - } /> + } + /> } /> - } /> - } /> + {/* } /> */} + {/* } /> */} } /> } /> } /> - } /> + {/* } /> */} diff --git a/client/src/api/communities.api.ts b/client/src/api/communities.api.ts index 482a92e..1a62230 100644 --- a/client/src/api/communities.api.ts +++ b/client/src/api/communities.api.ts @@ -6,7 +6,7 @@ import { PatchCommunity } from "../types/postApiTypes/patchCommunity.interface"; import { api } from "./api"; export const getCommunityPreviews = async ( - initData: string, + initData: string ): Promise => { try { const response = await api.get(`/communities`, { @@ -21,7 +21,7 @@ export const getCommunityPreviews = async ( export const createCommunity = async ( initData: string, - communityData: CreateCommunity, + communityData: CreateCommunity ): Promise => { try { const response = await api.post(`/communities`, communityData, { @@ -36,7 +36,7 @@ export const createCommunity = async ( export const patchCommunity = async ( initData: string, - patchCommunityData: PatchCommunity, + patchCommunityData: PatchCommunity ): Promise => { try { const response = await api.patch( @@ -44,7 +44,7 @@ export const patchCommunity = async ( patchCommunityData, { headers: { "X-Api-Token": initData }, - }, + } ); return response.data; } catch (error) { @@ -55,7 +55,7 @@ export const patchCommunity = async ( export const getCommunity = async ( initData: string, - id: string, + id: string ): Promise => { try { const response = await api.get(`/communities/id/${id}`, { @@ -70,7 +70,7 @@ export const getCommunity = async ( export const joinCommunity = async ( initData: string, - id: string, + id: string ): Promise => { try { const response = await api.post(`/communities/id/${id}`, { @@ -85,7 +85,7 @@ export const joinCommunity = async ( export const leaveCommunity = async ( initData: string, - id: string, + id: string ): Promise => { try { const response = await api.post(`/communities/id/${id}`, { @@ -101,7 +101,7 @@ export const leaveCommunity = async ( export const searchMembers = async ( initData: string, id: string, - query: string, + query: string ): Promise => { try { const response = await api.get( @@ -111,7 +111,7 @@ export const searchMembers = async ( q: query, }, headers: { "X-Api-Token": initData }, - }, + } ); return response.data; } catch (error) { @@ -123,14 +123,14 @@ export const searchMembers = async ( export const getMember = async ( initData: string, communityId: string, - memberId: string, -): Promise => { + memberId: string +): Promise => { try { - const response = await api.get( + const response = await api.get( `/communities/id/${communityId}/members/${memberId}`, { headers: { "X-Api-Token": initData }, - }, + } ); return response.data; } catch (error) { @@ -141,7 +141,7 @@ export const getMember = async ( export const getCommunityPreview = async ( initData: string, - id: string, + id: string ): Promise => { try { const response = await api.get(`/communities/id/${id}/preview`, { @@ -156,7 +156,7 @@ export const getCommunityPreview = async ( export const searchCommunities = async ( initData: string, - query: string, + query: string ): Promise => { try { const response = await api.get(`/communities/search`, { diff --git a/client/src/components/chatPreview.component.tsx b/client/src/components/chatPreview.component.tsx index c5cd5f6..0672755 100644 --- a/client/src/components/chatPreview.component.tsx +++ b/client/src/components/chatPreview.component.tsx @@ -1,11 +1,12 @@ import { useNavigate } from "react-router-dom"; -import { ChatPreviewData } from "../types/user.interface"; import { handleImageError } from "../utils/imageErrorHandler"; import DevImage from "../assets/dev.png"; import TrashBin from "../assets/trash_bin.svg"; import { motion } from "motion/react"; +import { Community } from "../types/community/community.interface"; + interface ChatPreviewComponentProps { - chatData: ChatPreviewData; + chatData: Community; view?: boolean; className?: string; underline?: boolean; @@ -59,7 +60,7 @@ const ChatPreviewComponent = (

    {props.chatData.name}

    - {props.chatData.usersAmount} участников + {props.chatData.membersCount} участников

    @@ -99,7 +100,7 @@ const ChatPreviewComponent = ( >

    {props.chatData.name}

    - {props.chatData.usersAmount} участников + {props.chatData.membersCount} участников

    @@ -141,7 +142,7 @@ const ChatPreviewComponent = ( >

    {props.chatData.name}

    - {props.chatData.usersAmount} участников + {props.chatData.membersCount} участников

    diff --git a/client/src/components/requireMembership.component.tsx b/client/src/components/requireMembership.component.tsx index 58cc4f1..7c1c6a3 100644 --- a/client/src/components/requireMembership.component.tsx +++ b/client/src/components/requireMembership.component.tsx @@ -1,7 +1,7 @@ import { useNavigate } from "react-router-dom"; import useInitDataStore from "../stores/InitData.store"; -import useChatPreview from "../hooks/useChatPreview"; import { useEffect } from "react"; +import useCommunity from "../hooks/communities/fetchHooks/useСommunity"; interface RequireMembershipComponentProps { children: React.ReactNode; @@ -12,15 +12,15 @@ const RequireMembershipComponent = (props: RequireMembershipComponentProps) => { const { initDataStartParam } = useInitDataStore(); const chatID = props.chatID ?? initDataStartParam; - const { isPending, data } = useChatPreview(chatID || ""); + const { isPending, data } = useCommunity(chatID || ""); useEffect(() => { - if (!isPending && data && data.isMember === false) { + if (!isPending && data) { navigate("/invite", { replace: true }); } }, [isPending, data, navigate]); - if (isPending || !data || data.isMember === false) { + if (isPending || !data) { return null; } diff --git a/client/src/hooks/useChatPreview.ts b/client/src/hooks/useChatPreview.ts deleted file mode 100644 index d20f8f1..0000000 --- a/client/src/hooks/useChatPreview.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getChatPreview } from "../api/api"; -import useInitDataStore from "../stores/InitData.store"; - -const useChatPreview = (chatId: string) => { - const { initData } = useInitDataStore(); - return useQuery({ - queryKey: [`chat/${chatId}/preview`, initData, chatId], - queryFn: () => getChatPreview(initData, chatId), - }); -}; - -export default useChatPreview; diff --git a/client/src/hooks/useChats.ts b/client/src/hooks/useChats.ts deleted file mode 100644 index d17e5a4..0000000 --- a/client/src/hooks/useChats.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getChats } from "../api/api"; -import useInitDataStore from "../stores/InitData.store"; - -const useChats = () => { - const { initData } = useInitDataStore(); - return useQuery({ - queryKey: ["chats", initData], - queryFn: () => getChats(initData), - }); -}; - -export default useChats; diff --git a/client/src/hooks/useCreateMe.ts b/client/src/hooks/useCreateMe.ts deleted file mode 100644 index 586e014..0000000 --- a/client/src/hooks/useCreateMe.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import useInitDataStore from "../stores/InitData.store"; -import { createMe } from "../api/api"; -import { UserData } from "../types/user.interface"; - -const useCreateMe = () => { - const { initData } = useInitDataStore(); - - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ( - profileData: Pick< - UserData, - "firstName" | "lastName" | "bio" | "role" | "company" - >, - ) => createMe(initData, profileData), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["user/me", initData] }); - queryClient.invalidateQueries({ - queryKey: ["user/me/preview", initData], - }); - }, - }); -}; - -export default useCreateMe; diff --git a/client/src/hooks/useJoinMe.ts b/client/src/hooks/useJoinMe.ts deleted file mode 100644 index abc4b9a..0000000 --- a/client/src/hooks/useJoinMe.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import useInitDataStore from "../stores/InitData.store"; -import { joinMe } from "../api/api"; - -const useJoinMe = () => { - const queryClient = useQueryClient(); - const { initData } = useInitDataStore(); - - return useMutation({ - mutationFn: (chatId: string) => joinMe(initData, chatId), - onSuccess: async (_, chatId) => { - await queryClient.refetchQueries({ - predicate: (query) => - Array.isArray(query.queryKey) && - query.queryKey[0] === "chats/preview", - }); - queryClient.invalidateQueries({ queryKey: ["chats"] }); - queryClient.invalidateQueries({ queryKey: [`chat/${chatId}`, chatId] }); - queryClient.invalidateQueries({ - queryKey: [`chat/${chatId}/preview`, initData, chatId], - }); - }, - }); -}; - -export default useJoinMe; diff --git a/client/src/hooks/useLeaveChat.ts b/client/src/hooks/useLeaveChat.ts deleted file mode 100644 index 8da4f7d..0000000 --- a/client/src/hooks/useLeaveChat.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { leaveChat } from "../api/api"; -import useInitDataStore from "../stores/InitData.store"; -import { ChatPreviewData } from "../types/user.interface"; - -const useLeaveChat = () => { - const { initData } = useInitDataStore(); - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (chatId: string) => leaveChat(initData, chatId), - onSuccess: (_, chatId) => { - queryClient.setQueryData( - ["chats/preview", initData, ""], - (old) => old?.filter((chat) => chat.id !== chatId) ?? [], - ); - }, - }); -}; - -export default useLeaveChat; diff --git a/client/src/hooks/useMe.ts b/client/src/hooks/useMe.ts deleted file mode 100644 index 4de5a7b..0000000 --- a/client/src/hooks/useMe.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import useInitDataStore from "../stores/InitData.store"; -import { getMe } from "../api/api"; - -const useMe = () => { - const { initData } = useInitDataStore(); - return useQuery({ - queryKey: ["user/me", initData], - queryFn: () => getMe(initData), - }); -}; - -export default useMe; diff --git a/client/src/hooks/useMePreview.ts b/client/src/hooks/useMePreview.ts deleted file mode 100644 index 15bbd27..0000000 --- a/client/src/hooks/useMePreview.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import useInitDataStore from "../stores/InitData.store"; -import { getMePreview } from "../api/api"; - -const useMePreview = () => { - const { initData } = useInitDataStore(); - return useQuery({ - queryKey: [`users/me/preview`, initData], - queryFn: () => getMePreview(initData), - }); -}; - -export default useMePreview; diff --git a/client/src/hooks/useSearchChats.ts b/client/src/hooks/useSearchChats.ts deleted file mode 100644 index 5a297ce..0000000 --- a/client/src/hooks/useSearchChats.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { keepPreviousData, useQuery } from "@tanstack/react-query"; -import { searchChats } from "../api/api"; -import useInitDataStore from "../stores/InitData.store"; - -const useSearchChats = (query: string) => { - const { initData } = useInitDataStore(); - - return useQuery({ - queryKey: ["chats/preview", initData, query], - queryFn: () => searchChats(initData, query), - placeholderData: keepPreviousData, - }); -}; - -export default useSearchChats; diff --git a/client/src/hooks/useSearchUsers.ts b/client/src/hooks/useSearchUsers.ts deleted file mode 100644 index 0acdeaa..0000000 --- a/client/src/hooks/useSearchUsers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { keepPreviousData, useQuery } from "@tanstack/react-query"; -import { searchInChat } from "../api/api"; -import useInitDataStore from "../stores/InitData.store"; - -const useSearchUsersInChat = (chatId: string, query: string) => { - const { initData } = useInitDataStore(); - - return useQuery({ - queryKey: [`chat/${chatId}`, initData, chatId, query], - queryFn: () => searchInChat(initData, chatId, query), - placeholderData: keepPreviousData, - }); -}; - -export default useSearchUsersInChat; diff --git a/client/src/hooks/useUpdateMe.ts b/client/src/hooks/useUpdateMe.ts deleted file mode 100644 index 01ec473..0000000 --- a/client/src/hooks/useUpdateMe.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useQueryClient, useMutation } from "@tanstack/react-query"; -import { postMe } from "../api/api"; -import useInitDataStore from "../stores/InitData.store"; -import { UserData } from "../types/user.interface"; -import { PatchMe } from "../types/postApiTypes/patchMe.interface"; -import { updateMe } from "../api/users.api"; - -const useUpdateMe = () => { - const { initData } = useInitDataStore(); - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (patchData: PatchMe) => updateMe(initData, patchData), - }); -}; - -const useUpdateMe = () => { - const { initData } = useInitDataStore(); - - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ( - profileData: Pick< - UserData, - "firstName" | "lastName" | "bio" | "role" | "company" - >, - ) => postMe(initData, profileData), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["user/me", initData] }); - queryClient.invalidateQueries({ - queryKey: ["user/me/preview", initData], - }); - }, - }); -}; - -export default useUpdateMe; diff --git a/client/src/pages/chats.page.tsx b/client/src/pages/communities.page.tsx similarity index 91% rename from client/src/pages/chats.page.tsx rename to client/src/pages/communities.page.tsx index 9e3ad42..12224cb 100644 --- a/client/src/pages/chats.page.tsx +++ b/client/src/pages/communities.page.tsx @@ -1,7 +1,6 @@ import { useEffect, useRef, useState } from "react"; import ProfileComponent from "../components/profile.component"; import SearchBarComponent from "../components/searchBar.component"; -import { ChatPreviewData } from "../types/user.interface"; import { openTelegramLink, popup } from "@telegram-apps/sdk-react"; import ChatPreviewComponent from "../components/chatPreview.component"; import DBBComponent from "../components/disableBackButton.component"; @@ -11,8 +10,9 @@ import { BOT_USERNAME } from "../shared/constants"; import NotFound from "../assets/notFound.svg"; import AddButton from "../assets/add_green.svg"; import { motion } from "motion/react"; -import useLeaveChat from "../hooks/useLeaveChat"; -import useSearchChats from "../hooks/useSearchChats"; +import useLeaveCommunity from "../hooks/communities/mutations/useLeaveCommunity"; +import useCommunitiesSearch from "../hooks/communities/search/useCommunitiesSearch"; +import { Community } from "../types/community/community.interface"; const containerVariants = { visible: { transition: { @@ -20,17 +20,17 @@ const containerVariants = { }, }, }; -const ChatsPage = () => { - const useLeaveChatMutation = useLeaveChat(); +const CommunitiesPage = () => { + const useLeaveChatMutation = useLeaveCommunity(); const { searchQuery, setSearchQuery, scroll, setScroll } = useChatsSearchStore(); - const { data: chats, isPending } = useSearchChats(searchQuery); + const { data: chats, isPending } = useCommunitiesSearch(searchQuery); const [loadedFirstTime, setLoadedFirstTime] = useState(false); - const deleteHandler = async (chatPreviewData: ChatPreviewData) => { + const deleteHandler = async (chatPreviewData: Community) => { popup .open({ message: @@ -119,7 +119,7 @@ const ChatsPage = () => { deleteHandler={() => deleteHandler(chat)} index={index} /> - ), + ) )} )} @@ -153,7 +153,7 @@ const ChatsPage = () => { index={index} animated /> - ), + ) )} )} @@ -179,4 +179,4 @@ const ChatsPage = () => { ); }; -export default ChatsPage; +export default CommunitiesPage; diff --git a/client/src/pages/chat.page.tsx b/client/src/pages/community.page.tsx similarity index 89% rename from client/src/pages/chat.page.tsx rename to client/src/pages/community.page.tsx index 53cb258..e05b497 100644 --- a/client/src/pages/chat.page.tsx +++ b/client/src/pages/community.page.tsx @@ -8,8 +8,8 @@ import useChatSearchStore from "../stores/chatSearch.store"; import ChatMemberCardComponent from "../components/chatMember.card.component"; import { motion } from "motion/react"; import NotFound from "../assets/notFound.svg"; -import useSearchUsersInChat from "../hooks/useSearchUsers"; -import useChatPreview from "../hooks/useChatPreview"; +import useSearchMembers from "../hooks/members/search/useSearchMembers"; +import useCommunity from "../hooks/communities/fetchHooks/useСommunity"; const containerVariants = { visible: { transition: { @@ -17,18 +17,18 @@ const containerVariants = { }, }, }; -const ChatPage = () => { +const CommunityPage = () => { const { chatId } = useParams(); const [loadedFirstTime, setLoadedFirstTime] = useState(false); const { getScroll, saveScroll, getSearchQuery, saveSearchQuery } = useChatSearchStore(); - const { data: chatData, isPending } = useSearchUsersInChat( + const { data: chatData, isPending } = useSearchMembers( chatId ?? "", - getSearchQuery(chatId ?? ""), + getSearchQuery(chatId ?? "") ); - const { data: previewChatData, isSuccess } = useChatPreview(chatId ?? ""); + const { data: previewChatData, isSuccess } = useCommunity(chatId ?? ""); const scrollContainerRef = useRef(null); const handleScroll = () => { @@ -84,7 +84,7 @@ const ChatPage = () => { {chatData?.map((user, index) => ( ))} @@ -101,7 +101,7 @@ const ChatPage = () => { { ); }; -export default ChatPage; +export default CommunityPage; diff --git a/client/src/pages/currentProfile.page.tsx b/client/src/pages/currentProfile.page.tsx index e424db9..21ff910 100644 --- a/client/src/pages/currentProfile.page.tsx +++ b/client/src/pages/currentProfile.page.tsx @@ -5,10 +5,12 @@ import { useNavigate } from "react-router-dom"; import EBBComponent from "../components/enableBackButtonComponent"; import DevImage from "../assets/dev.png"; import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; -import useMe from "../hooks/useMe"; +import useGetMe from "../hooks/users/fetchHooks/useGetMe"; const CurrentProfilePage = () => { - const { data: userData } = useMe(); + const { data } = useGetMe(); + const userData = data?.user; + const navigate = useNavigate(); const goToEditProfilePage = () => { @@ -37,21 +39,12 @@ const CurrentProfilePage = () => {
    - - + +
    ПОДРОБНЕЕ
    - +
    diff --git a/client/src/pages/editProfile.page.tsx b/client/src/pages/editProfile.page.tsx index 28af210..9873028 100644 --- a/client/src/pages/editProfile.page.tsx +++ b/client/src/pages/editProfile.page.tsx @@ -1,211 +1,214 @@ -import { useState, useEffect } from "react"; -import InputFieldComponent from "../components/inputField.component"; -import TextareaFieldComponent from "../components/textareaField.component"; -import { UserData, ProfileUserData } from "../types/user.interface"; -import { handleImageError } from "../utils/imageErrorHandler"; -import { useNavigate } from "react-router-dom"; -import EBBComponent from "../components/enableBackButtonComponent"; -import DevImage from "../assets/dev.png"; -import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; -import ButtonComponent from "../components/button.component"; -import useInitDataStore from "../stores/InitData.store"; -import useMe from "../hooks/useMe"; -import useUpdateMe from "../hooks/useUpdateMe"; - -const EditProfilePage = () => { - const updateMeMutation = useUpdateMe(); - - const { launchParams } = useInitDataStore(); - const navigate = useNavigate(); - - const { data } = useMe(); - const userData = { - firstName: data?.firstName ?? null, - lastName: data?.lastName ?? null, - company: data?.company ?? null, - role: data?.role ?? null, - bio: data?.bio ?? null, - avatar: data?.avatar ?? null, - id: data?.id ?? null, - telegramId: data?.telegramId ?? null, - telegramUsername: data?.telegramUsername ?? null, - }; - - const [profileData, setProfileData] = useState< - Omit - >({ ...userData }); - - const [isChanged, setIsChanged] = useState(false); - const [isFilled, setIsFilled] = useState(true); - const isProfileChanged = (newProfileData: ProfileUserData) => { - const isChanged = - newProfileData.firstName !== userData.firstName || - newProfileData.lastName !== userData.lastName || - newProfileData.company !== userData.company || - newProfileData.role !== userData.role || - newProfileData.bio !== userData.bio; - - return isChanged; - }; - - const isProfileFilled = ( - newProfileData: Pick< - UserData, - "firstName" | "lastName" | "bio" | "role" | "company" - >, - ) => { - const isFilled = - profileData.firstName?.trim() != "" && - profileData.lastName?.trim() !== "" && - profileData.company?.trim() !== "" && - profileData.role?.trim() !== "" && - newProfileData.bio?.trim() !== ""; - - return isFilled; - }; - - const MAX_INPUT_LENGTH = 40; - const MAX_TEXTAREA_LENGTH = 230; - - const handleInputChange = (field: keyof UserData, value: string) => { - setProfileData((prevState) => ({ - ...prevState, - [field]: value, - })); - }; - const goBack = () => { - navigate(-1); - }; - - useEffect(() => { - setIsChanged(isProfileChanged(profileData)); - setIsFilled(isProfileFilled(profileData)); - }, [profileData]); - - const [iosKeyboardOpen, setIosKeyboardOpen] = useState(false); - const [focusedFieldsCount, setFocusedFiledsCount] = useState(0); - const onFocusHandler = () => { - setFocusedFiledsCount(focusedFieldsCount + 1); - }; - const onBlurHandler = () => { - setFocusedFiledsCount(focusedFieldsCount - 1); - }; - const handleFieldsCount = () => { - if (launchParams?.tgWebAppPlatform === "ios") { - if (focusedFieldsCount > 0) { - setIosKeyboardOpen(true); - } - if (focusedFieldsCount === 0) { - setIosKeyboardOpen(false); - } - } - }; - - const handleClick = async () => { - if (!isFilled) return; - - if (isChanged) { - try { - await updateMeMutation.mutateAsync(profileData); - goBack(); - } catch (error) { - console.error("Ошибка при обновлении профиля", error); - } - } else { - goBack(); - } - }; - - useEffect(handleFieldsCount, [focusedFieldsCount]); - - return ( - -
    -
    -
    - -
    -
    - - handleInputChange("firstName", value) - } - value={profileData.firstName ?? ""} - maxLength={MAX_INPUT_LENGTH} - /> - handleInputChange("lastName", value)} - value={profileData.lastName ?? ""} - maxLength={MAX_INPUT_LENGTH} - /> - handleInputChange("company", value)} - value={profileData.company ?? "null"} - maxLength={MAX_INPUT_LENGTH} - /> - handleInputChange("role", value)} - value={profileData.role ?? ""} - maxLength={MAX_INPUT_LENGTH} - /> - handleInputChange("bio", value)} - title="Описание" - value={profileData.bio ?? ""} - maxLength={MAX_TEXTAREA_LENGTH} - /> - {launchParams?.tgWebAppPlatform === "ios" && ( -
    - -
    - )} - {launchParams?.tgWebAppPlatform !== "ios" && ( -
    - )} -
    -
    -
    - - {launchParams?.tgWebAppPlatform !== "ios" && ( -
    - -
    - )} -
    - ); -}; +// import { useState, useEffect } from "react"; +// import InputFieldComponent from "../components/inputField.component"; +// import TextareaFieldComponent from "../components/textareaField.component"; +// import { UserData, ProfileUserData } from "../types/user.interface"; +// import { handleImageError } from "../utils/imageErrorHandler"; +// import { useNavigate } from "react-router-dom"; +// import EBBComponent from "../components/enableBackButtonComponent"; +// import DevImage from "../assets/dev.png"; +// import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; +// import ButtonComponent from "../components/button.component"; +// import useInitDataStore from "../stores/InitData.store"; +// import useMe from "../hooks/useMe"; +// import useUpdateMe from "../hooks/useUpdateMe"; + +// const EditProfilePage = () => { +// const updateMeMutation = useUpdateMe(); + +// const { launchParams } = useInitDataStore(); +// const navigate = useNavigate(); + +// const { data } = useMe(); +// const userData = { +// firstName: data?.firstName ?? null, +// lastName: data?.lastName ?? null, +// company: data?.company ?? null, +// role: data?.role ?? null, +// bio: data?.bio ?? null, +// avatar: data?.avatar ?? null, +// id: data?.id ?? null, +// telegramId: data?.telegramId ?? null, +// telegramUsername: data?.telegramUsername ?? null, +// }; + +// const [profileData, setProfileData] = useState< +// Omit +// >({ ...userData }); + +// const [isChanged, setIsChanged] = useState(false); +// const [isFilled, setIsFilled] = useState(true); +// const isProfileChanged = (newProfileData: ProfileUserData) => { +// const isChanged = +// newProfileData.firstName !== userData.firstName || +// newProfileData.lastName !== userData.lastName || +// newProfileData.company !== userData.company || +// newProfileData.role !== userData.role || +// newProfileData.bio !== userData.bio; + +// return isChanged; +// }; + +// const isProfileFilled = ( +// newProfileData: Pick< +// UserData, +// "firstName" | "lastName" | "bio" | "role" | "company" +// >, +// ) => { +// const isFilled = +// profileData.firstName?.trim() != "" && +// profileData.lastName?.trim() !== "" && +// profileData.company?.trim() !== "" && +// profileData.role?.trim() !== "" && +// newProfileData.bio?.trim() !== ""; + +// return isFilled; +// }; + +// const MAX_INPUT_LENGTH = 40; +// const MAX_TEXTAREA_LENGTH = 230; + +// const handleInputChange = (field: keyof UserData, value: string) => { +// setProfileData((prevState) => ({ +// ...prevState, +// [field]: value, +// })); +// }; +// const goBack = () => { +// navigate(-1); +// }; + +// useEffect(() => { +// setIsChanged(isProfileChanged(profileData)); +// setIsFilled(isProfileFilled(profileData)); +// }, [profileData]); + +// const [iosKeyboardOpen, setIosKeyboardOpen] = useState(false); +// const [focusedFieldsCount, setFocusedFiledsCount] = useState(0); +// const onFocusHandler = () => { +// setFocusedFiledsCount(focusedFieldsCount + 1); +// }; +// const onBlurHandler = () => { +// setFocusedFiledsCount(focusedFieldsCount - 1); +// }; +// const handleFieldsCount = () => { +// if (launchParams?.tgWebAppPlatform === "ios") { +// if (focusedFieldsCount > 0) { +// setIosKeyboardOpen(true); +// } +// if (focusedFieldsCount === 0) { +// setIosKeyboardOpen(false); +// } +// } +// }; + +// const handleClick = async () => { +// if (!isFilled) return; + +// if (isChanged) { +// try { +// await updateMeMutation.mutateAsync(profileData); +// goBack(); +// } catch (error) { +// console.error("Ошибка при обновлении профиля", error); +// } +// } else { +// goBack(); +// } +// }; + +// useEffect(handleFieldsCount, [focusedFieldsCount]); + +// return ( +// +//
    +//
    +//
    +// +//
    +//
    +// +// handleInputChange("firstName", value) +// } +// value={profileData.firstName ?? ""} +// maxLength={MAX_INPUT_LENGTH} +// /> +// handleInputChange("lastName", value)} +// value={profileData.lastName ?? ""} +// maxLength={MAX_INPUT_LENGTH} +// /> +// handleInputChange("company", value)} +// value={profileData.company ?? "null"} +// maxLength={MAX_INPUT_LENGTH} +// /> +// handleInputChange("role", value)} +// value={profileData.role ?? ""} +// maxLength={MAX_INPUT_LENGTH} +// /> +// handleInputChange("bio", value)} +// title="Описание" +// value={profileData.bio ?? ""} +// maxLength={MAX_TEXTAREA_LENGTH} +// /> +// {launchParams?.tgWebAppPlatform === "ios" && ( +//
    +// +//
    +// )} +// {launchParams?.tgWebAppPlatform !== "ios" && ( +//
    +// )} +//
    +//
    +//
    + +// {launchParams?.tgWebAppPlatform !== "ios" && ( +//
    +// +//
    +// )} +//
    +// ); +// }; +// export default EditProfilePage; + +const EditProfilePage = () => <>Edit Profile Page; export default EditProfilePage; diff --git a/client/src/pages/initial.page.tsx b/client/src/pages/initial.page.tsx index 629f45b..84300a7 100644 --- a/client/src/pages/initial.page.tsx +++ b/client/src/pages/initial.page.tsx @@ -12,10 +12,10 @@ const InitialPage = () => { startParam !== null && startParam.length > 0 ) { - navigate("/chats", { replace: true }); + navigate("/communities", { replace: true }); navigate(`/chat/${startParam}`); } else { - navigate("/chats", { replace: true }); + navigate("/communities", { replace: true }); } }, []); return null; diff --git a/client/src/pages/invitation.page.tsx b/client/src/pages/invitation.page.tsx index b2c7a80..1f67cb1 100644 --- a/client/src/pages/invitation.page.tsx +++ b/client/src/pages/invitation.page.tsx @@ -1,60 +1,61 @@ -import { handleImageError } from "../utils/imageErrorHandler"; -import EBBComponent from "../components/enableBackButtonComponent"; -import { useNavigate } from "react-router-dom"; -import DevImage from "../assets/dev.png"; -import useInitDataStore from "../stores/InitData.store"; -import useJoinMe from "../hooks/useJoinMe"; -import useChatPreview from "../hooks/useChatPreview"; -const InvitationPage = () => { - const navigate = useNavigate(); - const { initDataStartParam: chatId } = useInitDataStore(); +// import { handleImageError } from "../utils/imageErrorHandler"; +// import EBBComponent from "../components/enableBackButtonComponent"; +// import { useNavigate } from "react-router-dom"; +// import DevImage from "../assets/dev.png"; +// import useInitDataStore from "../stores/InitData.store"; +// const InvitationPage = () => { +// const navigate = useNavigate(); +// const { initDataStartParam: chatId } = useInitDataStore(); - const joinMeMutation = useJoinMe(); - const handleAcceptInvitation = async () => { - await joinMeMutation.mutateAsync(chatId ?? ""); - navigate("/chats", { replace: true }); - navigate(`/chat/${chatId}`); - }; +// const joinMeMutation = useJoinMe(); +// const handleAcceptInvitation = async () => { +// await joinMeMutation.mutateAsync(chatId ?? ""); +// navigate("/chats", { replace: true }); +// navigate(`/chat/${chatId}`); +// }; - const { data: chatData } = useChatPreview(chatId ?? ""); +// const { data: chatData } = useChatPreview(chatId ?? ""); - return ( - -
    -
    -

    Расскажите о себе!

    -

    - Участники этого чата хотят узнать о вас больше -

    -
    +// return ( +// +//
    +//
    +//

    Расскажите о себе!

    +//

    +// Участники этого чата хотят узнать о вас больше +//

    +//
    -
    - -
    -

    - {chatData?.name ?? "название чата"} -

    -

    - Число участников: {chatData?.usersAmount} -

    -
    -
    +//
    +// +//
    +//

    +// {chatData?.name ?? "название чата"} +//

    +//

    +// Число участников: {chatData?.usersAmount} +//

    +//
    +//
    -
    - -
    -
    -
    - ); -}; +//
    +// +//
    +//
    +//
    +// ); +// }; +// export default InvitationPage; + +const InvitationPage = () => <>Invitaton page; export default InvitationPage; diff --git a/client/src/pages/profile.page.tsx b/client/src/pages/profile.page.tsx index e7a8233..98009df 100644 --- a/client/src/pages/profile.page.tsx +++ b/client/src/pages/profile.page.tsx @@ -7,11 +7,11 @@ import { handleImageError } from "../utils/imageErrorHandler"; import DevImage from "../assets/dev.png"; import ButtonComponent from "../components/button.component"; import TGWhite from "../assets/tg_white.svg"; -import useUser from "../hooks/useUser"; +import useMember from "../hooks/members/fetchHooks/useMember"; const ProfilePage = () => { - const { userId } = useParams(); - const { data: userData } = useUser(userId ?? ""); - + const { chatId, userId } = useParams(); + const { data } = useMember(chatId ?? "", userId ?? ""); + const userData = data?.user; return (
    @@ -44,23 +44,14 @@ const ProfilePage = () => {
    - - + +
    ПОДРОБНЕЕ
    - +
    diff --git a/client/src/pages/registration.page.tsx b/client/src/pages/registration.page.tsx index c56bcba..a9adb91 100644 --- a/client/src/pages/registration.page.tsx +++ b/client/src/pages/registration.page.tsx @@ -1,203 +1,205 @@ -import { useState, useEffect } from "react"; -import InputFieldComponent from "../components/inputField.component"; -import TextareaFieldComponent from "../components/textareaField.component"; -import { UserData } from "../types/user.interface"; -import { handleImageError } from "../utils/imageErrorHandler"; -import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; -import EBBComponent from "../components/enableBackButtonComponent"; -import { useNavigate } from "react-router-dom"; -import DevImage from "../assets/dev.png"; -import ButtonComponent from "../components/button.component"; -import useStartParamStore from "../stores/StartData.store"; -import useInitDataStore from "../stores/InitData.store"; -import useCreateMe from "../hooks/useCreateMe"; -import useJoinMe from "../hooks/useJoinMe"; -import useMePreview from "../hooks/useMePreview"; +// import { useState, useEffect } from "react"; +// import InputFieldComponent from "../components/inputField.component"; +// import TextareaFieldComponent from "../components/textareaField.component"; +// import { handleImageError } from "../utils/imageErrorHandler"; +// import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; +// import EBBComponent from "../components/enableBackButtonComponent"; +// import { useNavigate } from "react-router-dom"; +// import DevImage from "../assets/dev.png"; +// import ButtonComponent from "../components/button.component"; +// import useStartParamStore from "../stores/StartData.store"; +// import useInitDataStore from "../stores/InitData.store"; -const RegistrationPage = () => { - //mutations - const createMeMutation = useCreateMe(); - const joinMeMutation = useJoinMe(); +// const RegistrationPage = () => { +// //mutations +// const createMeMutation = useCreateMe(); +// const joinMeMutation = useJoinMe(); + +// const navigate = useNavigate(); - const navigate = useNavigate(); +// //tg params +// const { startParam } = useStartParamStore(); +// const { initDataUser, launchParams } = useInitDataStore(); +// const avatar = initDataUser?.photo_url; - //tg params - const { startParam } = useStartParamStore(); - const { initDataUser, launchParams } = useInitDataStore(); - const avatar = initDataUser?.photo_url; +// //form validation data +// const [profileData, setProfileData] = useState< +// Pick +// >({ +// firstName: initDataUser?.first_name || "", +// lastName: initDataUser?.last_name || "", +// company: "", +// role: "", +// bio: "", +// }); +// const [isFilled, setIsFilled] = useState(false); +// const isProfileFilled = () => { +// const isFilled = +// profileData.firstName?.trim() != "" && +// profileData.lastName?.trim() !== "" && +// profileData.company?.trim() !== "" && +// profileData.role?.trim() !== "" && +// profileData.bio?.trim() !== ""; - //form validation data - const [profileData, setProfileData] = useState< - Pick - >({ - firstName: initDataUser?.first_name || "", - lastName: initDataUser?.last_name || "", - company: "", - role: "", - bio: "", - }); - const [isFilled, setIsFilled] = useState(false); - const isProfileFilled = () => { - const isFilled = - profileData.firstName?.trim() != "" && - profileData.lastName?.trim() !== "" && - profileData.company?.trim() !== "" && - profileData.role?.trim() !== "" && - profileData.bio?.trim() !== ""; +// return isFilled; +// }; - return isFilled; - }; +// const MAX_INPUT_LENGTH = 40; +// const MAX_TEXTAREA_LENGTH = 230; - const MAX_INPUT_LENGTH = 40; - const MAX_TEXTAREA_LENGTH = 230; +// const handleInputChange = (field: keyof UserData, value: string) => { +// setProfileData((prevState) => ({ +// ...prevState, +// [field]: value, +// })); +// }; - const handleInputChange = (field: keyof UserData, value: string) => { - setProfileData((prevState) => ({ - ...prevState, - [field]: value, - })); - }; +// const handleRegister = async () => { +// try { +// await createMeMutation.mutateAsync(profileData); +// navigate("/chats", { replace: true }); +// if (startParam !== undefined && startParam.length > 0) { +// await joinMeMutation.mutateAsync(startParam); +// navigate(`/chat/${startParam}`); +// } else { +// navigate("/chats", { replace: true }); +// } +// } catch (error) { +// console.error("Ошибка при создании пользователя", error); +// } +// }; - const handleRegister = async () => { - try { - await createMeMutation.mutateAsync(profileData); - navigate("/chats", { replace: true }); - if (startParam !== undefined && startParam.length > 0) { - await joinMeMutation.mutateAsync(startParam); - navigate(`/chat/${startParam}`); - } else { - navigate("/chats", { replace: true }); - } - } catch (error) { - console.error("Ошибка при создании пользователя", error); - } - }; +// const { data: preview } = useMePreview(); +// useEffect(() => { +// if (preview?.bio) { +// setProfileData((prevState) => ({ +// ...prevState, +// bio: preview.bio, +// })); +// } +// }, [preview?.bio]); - const { data: preview } = useMePreview(); - useEffect(() => { - if (preview?.bio) { - setProfileData((prevState) => ({ - ...prevState, - bio: preview.bio, - })); - } - }, [preview?.bio]); +// useEffect(() => { +// setIsFilled(isProfileFilled()); +// }, [profileData]); - useEffect(() => { - setIsFilled(isProfileFilled()); - }, [profileData]); +// const [iosKeyboardOpen, setIosKeyboardOpen] = useState(false); +// const [focusedFieldsCount, setFocusedFiledsCount] = useState(0); +// const onFocusHandler = () => { +// setFocusedFiledsCount(focusedFieldsCount + 1); +// }; +// const onBlurHandler = () => { +// setFocusedFiledsCount(focusedFieldsCount - 1); +// }; +// const handleFieldsCount = () => { +// if (launchParams?.tgWebAppPlatform === "ios") { +// if (focusedFieldsCount > 0) { +// setIosKeyboardOpen(true); +// } +// if (focusedFieldsCount === 0) { +// setIosKeyboardOpen(false); +// } +// } +// }; +// useEffect(handleFieldsCount, [focusedFieldsCount]); +// return ( +// +//
    +//
    +// +//
    +//
    +// handleInputChange("firstName", value)} +// value={profileData.firstName ?? ""} +// maxLength={MAX_INPUT_LENGTH} +// /> +// handleInputChange("lastName", value)} +// value={profileData.lastName ?? ""} +// maxLength={MAX_INPUT_LENGTH} +// /> +// handleInputChange("company", value)} +// value={profileData.company ?? ""} +// maxLength={MAX_INPUT_LENGTH} +// /> +// handleInputChange("role", value)} +// value={profileData.role ?? ""} +// maxLength={MAX_INPUT_LENGTH} +// /> +// handleInputChange("bio", value)} +// title="Описание" +// value={profileData.bio ?? ""} +// maxLength={MAX_TEXTAREA_LENGTH} +// /> +//
    - const [iosKeyboardOpen, setIosKeyboardOpen] = useState(false); - const [focusedFieldsCount, setFocusedFiledsCount] = useState(0); - const onFocusHandler = () => { - setFocusedFiledsCount(focusedFieldsCount + 1); - }; - const onBlurHandler = () => { - setFocusedFiledsCount(focusedFieldsCount - 1); - }; - const handleFieldsCount = () => { - if (launchParams?.tgWebAppPlatform === "ios") { - if (focusedFieldsCount > 0) { - setIosKeyboardOpen(true); - } - if (focusedFieldsCount === 0) { - setIosKeyboardOpen(false); - } - } - }; - useEffect(handleFieldsCount, [focusedFieldsCount]); - return ( - -
    -
    - -
    -
    - handleInputChange("firstName", value)} - value={profileData.firstName ?? ""} - maxLength={MAX_INPUT_LENGTH} - /> - handleInputChange("lastName", value)} - value={profileData.lastName ?? ""} - maxLength={MAX_INPUT_LENGTH} - /> - handleInputChange("company", value)} - value={profileData.company ?? ""} - maxLength={MAX_INPUT_LENGTH} - /> - handleInputChange("role", value)} - value={profileData.role ?? ""} - maxLength={MAX_INPUT_LENGTH} - /> - handleInputChange("bio", value)} - title="Описание" - value={profileData.bio ?? ""} - maxLength={MAX_TEXTAREA_LENGTH} - /> -
    +// {launchParams?.tgWebAppPlatform !== "ios" && ( +//
    +// )} +// {launchParams?.tgWebAppPlatform === "ios" && ( +//
    { +// if (isFilled) handleRegister(); +// }} +// className="flex w-full justify-center pt-[20px]" +// > +// { +// if (isFilled) handleRegister(); +// }} +// state={isFilled ? "active" : "disabled"} +// /> +//
    +// )} +//
    - {launchParams?.tgWebAppPlatform !== "ios" && ( -
    - )} - {launchParams?.tgWebAppPlatform === "ios" && ( -
    { - if (isFilled) handleRegister(); - }} - className="flex w-full justify-center pt-[20px]" - > - { - if (isFilled) handleRegister(); - }} - state={isFilled ? "active" : "disabled"} - /> -
    - )} -
    +// {launchParams?.tgWebAppPlatform !== "ios" && ( +//
    +// { +// if (isFilled) handleRegister(); +// }} +// state={isFilled ? "active" : "disabled"} +// /> +//
    +// )} +//
    +// ); +// }; +// export default RegistrationPage; - {launchParams?.tgWebAppPlatform !== "ios" && ( -
    - { - if (isFilled) handleRegister(); - }} - state={isFilled ? "active" : "disabled"} - /> -
    - )} -
    - ); +const RegistrationPage = () => { + return <>registration; }; + export default RegistrationPage; diff --git a/client/src/types/user/user.interface.ts b/client/src/types/user/user.interface.ts index 61567fd..31d884f 100644 --- a/client/src/types/user/user.interface.ts +++ b/client/src/types/user/user.interface.ts @@ -1,6 +1,7 @@ export interface User { avatar: string; firstName: string; + lastName: string; id: string; telegramId: number; telegramUsername: string; diff --git a/client/src/components/protectedRoute.component.tsx b/client/src/utils/protectedRoute.tsx similarity index 64% rename from client/src/components/protectedRoute.component.tsx rename to client/src/utils/protectedRoute.tsx index 9f0bf94..4913f19 100644 --- a/client/src/components/protectedRoute.component.tsx +++ b/client/src/utils/protectedRoute.tsx @@ -1,11 +1,12 @@ import { Navigate, Outlet } from "react-router-dom"; -import useMePreview from "../hooks/useMePreview"; +import useGetMe from "../hooks/users/fetchHooks/useGetMe"; const ProtectedRoute = () => { - const { isPending, data } = useMePreview(); + const { isPending, data } = useGetMe(); if (isPending) { return null; } - if (!data?.isRegistered) { + + if (!data) { return ; } return ; From 4374f3c1679af6a8e8bc209e3c8e37d11750c817 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 8 May 2025 01:30:40 +0300 Subject: [PATCH 17/78] another api fix --- client/src/App.tsx | 4 +- client/src/api/communities.api.ts | 2 +- .../components/chatMember.card.component.tsx | 59 ++++++++----------- .../src/components/chatMember.component.tsx | 49 --------------- .../src/components/chatPreview.component.tsx | 19 ++---- .../requireMembership.component.tsx | 17 +++--- client/src/pages/communities.page.tsx | 12 +++- client/src/pages/community.page.tsx | 32 ++++++---- client/src/pages/profile.page.tsx | 4 +- 9 files changed, 72 insertions(+), 126 deletions(-) delete mode 100644 client/src/components/chatMember.component.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index eee7a68..7c843a4 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -49,9 +49,9 @@ function App() { }> } /> - } /> + } /> } /> } /> diff --git a/client/src/api/communities.api.ts b/client/src/api/communities.api.ts index 1a62230..d023291 100644 --- a/client/src/api/communities.api.ts +++ b/client/src/api/communities.api.ts @@ -127,7 +127,7 @@ export const getMember = async ( ): Promise => { try { const response = await api.get( - `/communities/id/${communityId}/members/${memberId}`, + `/communities/id/${communityId}/members/id/${memberId}`, { headers: { "X-Api-Token": initData }, } diff --git a/client/src/components/chatMember.card.component.tsx b/client/src/components/chatMember.card.component.tsx index b96cafa..7c2ec59 100644 --- a/client/src/components/chatMember.card.component.tsx +++ b/client/src/components/chatMember.card.component.tsx @@ -1,12 +1,10 @@ import { motion } from "framer-motion"; -import { useNavigate } from "react-router-dom"; -import { ChatMemverComponentProps } from "./chatMember.component"; import { handleImageError } from "../utils/imageErrorHandler"; import DevImage from "../assets/dev.png"; -import useUserStore from "../stores/user.store"; import NavImage from "../assets/navigation.svg"; import Case from "../assets/case.svg"; import Person from "../assets/person.svg"; +import { Member } from "../types/member/member.interface"; const cardVariants = { hidden: { opacity: 0, y: 20 }, @@ -21,29 +19,18 @@ const cardVariants = { }), }; -const ChatMemberCardComponent = ( - props: ChatMemverComponentProps & { - index: number; - animated?: boolean; - onAnimationComplete?: () => void; - } -) => { - const navigate = useNavigate(); - const userStore = useUserStore(); - - const goToProfile = () => { - if (props.userData.id !== userStore.userData.id) { - navigate(`/profile/${props.userData.id}`); - } else { - navigate(`/profile`); - } - }; - +const ChatMemberCardComponent = (props: { + onClick?: () => void; + index: number; + animated?: boolean; + onAnimationComplete?: () => void; + member: Member; +}) => { if (props.animated) { return (

    - {props.userData.firstName} {props.userData.lastName} + {props.member.user.firstName} {props.member.user.lastName}

    - {props.userData.company} + компания

    - {props.userData.role} + роль

    @@ -78,9 +65,10 @@ const ChatMemberCardComponent = (

    - {props.userData.bio && props.userData.bio.length > 90 + {/* {props.userData.bio && props.userData.bio.length > 90 ? props.userData.bio.slice(0, 90) + "..." - : props.userData.bio} + : props.userData.bio} */} + биография

    @@ -90,28 +78,28 @@ const ChatMemberCardComponent = ( return (
  • - {props.userData.firstName} {props.userData.lastName} + {props.member.user.firstName} {props.member.user.lastName}

    - {props.userData.company} + компания

    - {props.userData.role} + роль

    @@ -120,9 +108,10 @@ const ChatMemberCardComponent = (

    - {props.userData.bio && props.userData.bio.length > 90 + {/* {props.userData.bio && props.userData.bio.length > 90 ? props.userData.bio.slice(0, 90) + "..." - : props.userData.bio} + : props.userData.bio} */} + биография

  • diff --git a/client/src/components/chatMember.component.tsx b/client/src/components/chatMember.component.tsx deleted file mode 100644 index 8d5891c..0000000 --- a/client/src/components/chatMember.component.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useNavigate } from "react-router-dom"; -import { UserData } from "../types/user.interface"; -import { handleImageError } from "../utils/imageErrorHandler"; -import NavArrow from "../assets/navigation.svg"; -import DevImage from "../assets/dev.png"; -export interface ChatMemverComponentProps { - userData: UserData; -} -const ChatMemberComponent = (props: ChatMemverComponentProps) => { - const navigate = useNavigate(); - return ( -
  • { - navigate(`/profile/${props.userData.id}`); - }} - > -
    - -
    -
    -

    - {props.userData.firstName} {props.userData.lastName} -

    -

    - {props.userData.company} -

    - -

    - {props.userData.role} -

    -

    - {props.userData.bio && props.userData.bio.length > 30 - ? props.userData.bio.slice(0, 30) + "..." - : props.userData.bio} -

    -
    - -
    -
    -
  • - ); -}; - -export default ChatMemberComponent; diff --git a/client/src/components/chatPreview.component.tsx b/client/src/components/chatPreview.component.tsx index 0672755..41d7376 100644 --- a/client/src/components/chatPreview.component.tsx +++ b/client/src/components/chatPreview.component.tsx @@ -1,4 +1,3 @@ -import { useNavigate } from "react-router-dom"; import { handleImageError } from "../utils/imageErrorHandler"; import DevImage from "../assets/dev.png"; import TrashBin from "../assets/trash_bin.svg"; @@ -11,6 +10,7 @@ interface ChatPreviewComponentProps { className?: string; underline?: boolean; deleteHandler?: () => void; + onClick?: () => void; } const chatPreviewVariants = { @@ -32,7 +32,6 @@ const ChatPreviewComponent = ( animated?: boolean; } ) => { - const navigate = useNavigate(); if (props.view) { return ( { - navigate(`/chat/${props.chatData.id}`); - }} + onClick={props.onClick} />
    { - navigate(`/chat/${props.chatData.id}`); - }} + onClick={props.onClick} >

    {props.chatData.name}

    @@ -125,9 +120,7 @@ const ChatPreviewComponent = ( src={props.chatData.avatar || DevImage} onError={handleImageError} className="w-[60px] h-[60px] rounded-full aspect-square object-cover" - onClick={() => { - navigate(`/chat/${props.chatData.id}`); - }} + onClick={props.onClick} />

    { - navigate(`/chat/${props.chatData.id}`); - }} + onClick={props.onClick} >

    {props.chatData.name}

    diff --git a/client/src/components/requireMembership.component.tsx b/client/src/components/requireMembership.component.tsx index 7c1c6a3..4edc232 100644 --- a/client/src/components/requireMembership.component.tsx +++ b/client/src/components/requireMembership.component.tsx @@ -1,6 +1,5 @@ import { useNavigate } from "react-router-dom"; import useInitDataStore from "../stores/InitData.store"; -import { useEffect } from "react"; import useCommunity from "../hooks/communities/fetchHooks/useСommunity"; interface RequireMembershipComponentProps { @@ -12,19 +11,17 @@ const RequireMembershipComponent = (props: RequireMembershipComponentProps) => { const { initDataStartParam } = useInitDataStore(); const chatID = props.chatID ?? initDataStartParam; - const { isPending, data } = useCommunity(chatID || ""); + const { isPending, isSuccess } = useCommunity(chatID || ""); - useEffect(() => { - if (!isPending && data) { - navigate("/invite", { replace: true }); - } - }, [isPending, data, navigate]); - - if (isPending || !data) { + if (isPending) { return null; } - return <>{props.children}; + if (!initDataStartParam || (initDataStartParam === chatID && isSuccess)) { + return <>{props.children}; + } + + navigate("/invite"); }; export default RequireMembershipComponent; diff --git a/client/src/pages/communities.page.tsx b/client/src/pages/communities.page.tsx index 12224cb..3bad22f 100644 --- a/client/src/pages/communities.page.tsx +++ b/client/src/pages/communities.page.tsx @@ -4,7 +4,7 @@ import SearchBarComponent from "../components/searchBar.component"; import { openTelegramLink, popup } from "@telegram-apps/sdk-react"; import ChatPreviewComponent from "../components/chatPreview.component"; import DBBComponent from "../components/disableBackButton.component"; -import { Outlet } from "react-router-dom"; +import { Outlet, useNavigate } from "react-router-dom"; import useChatsSearchStore from "../stores/chatsSearch.store"; import { BOT_USERNAME } from "../shared/constants"; import NotFound from "../assets/notFound.svg"; @@ -21,6 +21,8 @@ const containerVariants = { }, }; const CommunitiesPage = () => { + const navigate = useNavigate(); + const useLeaveChatMutation = useLeaveCommunity(); const { searchQuery, setSearchQuery, scroll, setScroll } = @@ -30,6 +32,10 @@ const CommunitiesPage = () => { const [loadedFirstTime, setLoadedFirstTime] = useState(false); + const goToCommunity = (communityId: string) => { + navigate(`/community/${communityId}`); + }; + const deleteHandler = async (chatPreviewData: Community) => { popup .open({ @@ -109,6 +115,7 @@ const CommunitiesPage = () => { key={chat.id} chatData={chat} deleteHandler={() => deleteHandler(chat)} + onClick={() => goToCommunity(chat.id)} underline index={index} /> @@ -117,6 +124,7 @@ const CommunitiesPage = () => { key={chat.id} chatData={chat} deleteHandler={() => deleteHandler(chat)} + onClick={() => goToCommunity(chat.id)} index={index} /> ) @@ -136,6 +144,7 @@ const CommunitiesPage = () => { key={chat.id} chatData={chat} deleteHandler={() => deleteHandler(chat)} + onClick={() => goToCommunity(chat.id)} underline index={index} animated @@ -147,6 +156,7 @@ const CommunitiesPage = () => { /> ) : ( goToCommunity(chat.id)} key={chat.id} chatData={chat} deleteHandler={() => deleteHandler(chat)} diff --git a/client/src/pages/community.page.tsx b/client/src/pages/community.page.tsx index e05b497..e415c3d 100644 --- a/client/src/pages/community.page.tsx +++ b/client/src/pages/community.page.tsx @@ -1,4 +1,4 @@ -import { useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import EBBComponent from "../components/enableBackButtonComponent"; import RequireMembershipComponent from "../components/requireMembership.component"; import SearchBarComponent from "../components/searchBar.component"; @@ -18,22 +18,29 @@ const containerVariants = { }, }; const CommunityPage = () => { - const { chatId } = useParams(); + const navigate = useNavigate(); + + const { communityId } = useParams(); const [loadedFirstTime, setLoadedFirstTime] = useState(false); const { getScroll, saveScroll, getSearchQuery, saveSearchQuery } = useChatSearchStore(); const { data: chatData, isPending } = useSearchMembers( - chatId ?? "", - getSearchQuery(chatId ?? "") + communityId ?? "", + getSearchQuery(communityId ?? "") ); - const { data: previewChatData, isSuccess } = useCommunity(chatId ?? ""); + + const goToProfile = (memberId: string) => { + navigate(`/community/${communityId}/member/${memberId}`); + }; + + const { data: previewChatData, isSuccess } = useCommunity(communityId ?? ""); const scrollContainerRef = useRef(null); const handleScroll = () => { if (scrollContainerRef.current) { - saveScroll(chatId ?? "", scrollContainerRef.current.scrollTop); + saveScroll(communityId ?? "", scrollContainerRef.current.scrollTop); } }; @@ -41,7 +48,7 @@ const CommunityPage = () => { const timer = setTimeout(() => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollTo({ - top: getScroll(chatId ?? ""), + top: getScroll(communityId ?? ""), behavior: "smooth", }); } @@ -58,7 +65,7 @@ const CommunityPage = () => { return ( - +

    { )} saveSearchQuery(chatId ?? "", value)} + value={getSearchQuery(communityId ?? "")} + inputHandler={(value) => saveSearchQuery(communityId ?? "", value)} placeholder="Поиск участников" className="mt-[20px]" /> @@ -83,9 +90,10 @@ const CommunityPage = () => {
      {chatData?.map((user, index) => ( goToProfile(user.user.id)} index={index} key={user.user.id} - userData={user} + member={user} /> ))}
    @@ -102,7 +110,7 @@ const CommunityPage = () => { animated index={index} key={user.user.id} - userData={user} + member={user} onAnimationComplete={ index === chatData.length - 1 ? () => setLoadedFirstTime(true) diff --git a/client/src/pages/profile.page.tsx b/client/src/pages/profile.page.tsx index 98009df..aec1ed9 100644 --- a/client/src/pages/profile.page.tsx +++ b/client/src/pages/profile.page.tsx @@ -9,8 +9,8 @@ import ButtonComponent from "../components/button.component"; import TGWhite from "../assets/tg_white.svg"; import useMember from "../hooks/members/fetchHooks/useMember"; const ProfilePage = () => { - const { chatId, userId } = useParams(); - const { data } = useMember(chatId ?? "", userId ?? ""); + const { communityId, memberId } = useParams(); + const { data } = useMember(communityId ?? "", memberId ?? ""); const userData = data?.user; return ( From 32287300901f045fda2b67ce92734f387bbefcfc Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 8 May 2025 02:02:46 +0300 Subject: [PATCH 18/78] member card --- .../components/chatMember.card.component.tsx | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/client/src/components/chatMember.card.component.tsx b/client/src/components/chatMember.card.component.tsx index 7c2ec59..8f60dd6 100644 --- a/client/src/components/chatMember.card.component.tsx +++ b/client/src/components/chatMember.card.component.tsx @@ -5,6 +5,7 @@ import NavImage from "../assets/navigation.svg"; import Case from "../assets/case.svg"; import Person from "../assets/person.svg"; import { Member } from "../types/member/member.interface"; +import { useEffect, useState } from "react"; const cardVariants = { hidden: { opacity: 0, y: 20 }, @@ -26,6 +27,24 @@ const ChatMemberCardComponent = (props: { onAnimationComplete?: () => void; member: Member; }) => { + const fields = props.member.config.fields; + + const [textInputs, setTextInputs] = useState([]); + const [textArea, setTextArea] = useState(""); + useEffect(() => { + const textInputs = Object.entries(fields) + .filter(([_, field]) => field.type === "textinput") + .map(([key, field]) => ({ key, ...field })); + setTextInputs([ + textInputs[0].textinput!.value, + textInputs[1].textinput!.value, + ]); + const textAreas = Object.entries(fields) + .filter(([_, field]) => field.type === "textarea") + .map(([key, field]) => ({ key, ...field })); + setTextArea(textAreas[0].textarea!.value); + }, []); + if (props.animated) { return (

    - - компания + {textInputs[0]}

    - - роль + {textInputs[1]}

    @@ -65,10 +82,9 @@ const ChatMemberCardComponent = (props: {

    - {/* {props.userData.bio && props.userData.bio.length > 90 - ? props.userData.bio.slice(0, 90) + "..." - : props.userData.bio} */} - биография + {textArea && textArea.length > 90 + ? textArea.slice(0, 90) + "..." + : textArea}

    @@ -94,12 +110,10 @@ const ChatMemberCardComponent = (props: {

    - - компания + {textInputs[0]}

    - - роль + {textInputs[1]}

    @@ -108,10 +122,9 @@ const ChatMemberCardComponent = (props: {

    - {/* {props.userData.bio && props.userData.bio.length > 90 - ? props.userData.bio.slice(0, 90) + "..." - : props.userData.bio} */} - биография + {textArea && textArea.length > 90 + ? textArea.slice(0, 90) + "..." + : textArea}

    From 483056f11529cb179d444b3ef1b9034c950dd936 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 8 May 2025 02:14:32 +0300 Subject: [PATCH 19/78] extract fields hook --- .../components/chatMember.card.component.tsx | 32 +++++-------------- client/src/hooks/utils/extractFields.ts | 23 +++++++++++++ 2 files changed, 31 insertions(+), 24 deletions(-) create mode 100644 client/src/hooks/utils/extractFields.ts diff --git a/client/src/components/chatMember.card.component.tsx b/client/src/components/chatMember.card.component.tsx index 8f60dd6..a647999 100644 --- a/client/src/components/chatMember.card.component.tsx +++ b/client/src/components/chatMember.card.component.tsx @@ -2,10 +2,8 @@ import { motion } from "framer-motion"; import { handleImageError } from "../utils/imageErrorHandler"; import DevImage from "../assets/dev.png"; import NavImage from "../assets/navigation.svg"; -import Case from "../assets/case.svg"; -import Person from "../assets/person.svg"; import { Member } from "../types/member/member.interface"; -import { useEffect, useState } from "react"; +import { useExtractFields } from "../hooks/utils/extractFields"; const cardVariants = { hidden: { opacity: 0, y: 20 }, @@ -29,21 +27,7 @@ const ChatMemberCardComponent = (props: { }) => { const fields = props.member.config.fields; - const [textInputs, setTextInputs] = useState([]); - const [textArea, setTextArea] = useState(""); - useEffect(() => { - const textInputs = Object.entries(fields) - .filter(([_, field]) => field.type === "textinput") - .map(([key, field]) => ({ key, ...field })); - setTextInputs([ - textInputs[0].textinput!.value, - textInputs[1].textinput!.value, - ]); - const textAreas = Object.entries(fields) - .filter(([_, field]) => field.type === "textarea") - .map(([key, field]) => ({ key, ...field })); - setTextArea(textAreas[0].textarea!.value); - }, []); + const { textInputs, textAreas } = useExtractFields(fields); if (props.animated) { return ( @@ -82,9 +66,9 @@ const ChatMemberCardComponent = (props: {

    - {textArea && textArea.length > 90 - ? textArea.slice(0, 90) + "..." - : textArea} + {textAreas[0] && textAreas[0].length > 90 + ? textAreas[0].slice(0, 90) + "..." + : textAreas[0]}

    @@ -122,9 +106,9 @@ const ChatMemberCardComponent = (props: {

    - {textArea && textArea.length > 90 - ? textArea.slice(0, 90) + "..." - : textArea} + {textAreas[0] && textAreas[0].length > 90 + ? textAreas[0].slice(0, 90) + "..." + : textAreas[0]}

    diff --git a/client/src/hooks/utils/extractFields.ts b/client/src/hooks/utils/extractFields.ts new file mode 100644 index 0000000..fc0040e --- /dev/null +++ b/client/src/hooks/utils/extractFields.ts @@ -0,0 +1,23 @@ +// hooks/useExtractFields.js +import { useState, useEffect } from "react"; +import { FieldValue } from "../../types/fields/fieldValue.interface"; + +export const useExtractFields = (fields: Record) => { + const [textInputs, setTextInputs] = useState([]); + const [textAreas, setTextAreas] = useState([]); + + useEffect(() => { + const extractedTextInputs = Object.entries(fields) + .filter(([_, field]) => field.type === "textinput") + .map(([key, field]) => ({ key, ...field })); + + const extractedTextAreas = Object.entries(fields) + .filter(([_, field]) => field.type === "textarea") + .map(([key, field]) => ({ key, ...field })); + + setTextInputs(extractedTextInputs.map((field) => field.textinput!.value)); + setTextAreas(extractedTextAreas.map((field) => field.textarea!.value)); + }, [fields]); + + return { textInputs, textAreas }; +}; From 9e1baf14c1493d0f51e0691784c454544c78f4cb Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 8 May 2025 03:41:50 +0300 Subject: [PATCH 20/78] render refactor, fields hook refactor --- .../components/chatMember.card.component.tsx | 27 ++++++++------ .../src/components/chatPreview.component.tsx | 2 +- client/src/hooks/utils/extractFields.ts | 37 +++++++++++++------ client/src/pages/communities.page.tsx | 1 - client/src/pages/community.page.tsx | 7 +--- client/src/pages/initial.page.tsx | 1 - client/src/pages/profile.page.tsx | 28 +++++++++++--- 7 files changed, 66 insertions(+), 37 deletions(-) diff --git a/client/src/components/chatMember.card.component.tsx b/client/src/components/chatMember.card.component.tsx index a647999..aefe218 100644 --- a/client/src/components/chatMember.card.component.tsx +++ b/client/src/components/chatMember.card.component.tsx @@ -26,9 +26,16 @@ const ChatMemberCardComponent = (props: { member: Member; }) => { const fields = props.member.config.fields; - const { textInputs, textAreas } = useExtractFields(fields); + const firstTextInput = textInputs[0]?.value || ""; + const secondTextInput = textInputs[1]?.value || ""; + const textArea = textAreas[0]?.value || ""; + + if (!fields) { + return null; + } + if (props.animated) { return (

    - {textInputs[0]} + {firstTextInput}

    - {textInputs[1]} + {secondTextInput}

    @@ -66,9 +73,7 @@ const ChatMemberCardComponent = (props: {

    - {textAreas[0] && textAreas[0].length > 90 - ? textAreas[0].slice(0, 90) + "..." - : textAreas[0]} + {textArea.length > 90 ? textArea.slice(0, 90) + "..." : textArea}

    @@ -94,10 +99,10 @@ const ChatMemberCardComponent = (props: {

    - {textInputs[0]} + {firstTextInput}

    - {textInputs[1]} + {secondTextInput}

    @@ -106,14 +111,14 @@ const ChatMemberCardComponent = (props: {

    - {textAreas[0] && textAreas[0].length > 90 - ? textAreas[0].slice(0, 90) + "..." - : textAreas[0]} + {textArea.length > 90 ? textArea.slice(0, 90) + "..." : textArea}

    ); } + + return null; }; export default ChatMemberCardComponent; diff --git a/client/src/components/chatPreview.component.tsx b/client/src/components/chatPreview.component.tsx index 41d7376..84ebb22 100644 --- a/client/src/components/chatPreview.component.tsx +++ b/client/src/components/chatPreview.component.tsx @@ -59,7 +59,7 @@ const ChatPreviewComponent = (

    {props.chatData.name}

    - {props.chatData.membersCount} участников + {props.chatData.members.length} участников

    diff --git a/client/src/hooks/utils/extractFields.ts b/client/src/hooks/utils/extractFields.ts index fc0040e..fef3f40 100644 --- a/client/src/hooks/utils/extractFields.ts +++ b/client/src/hooks/utils/extractFields.ts @@ -1,22 +1,35 @@ -// hooks/useExtractFields.js import { useState, useEffect } from "react"; import { FieldValue } from "../../types/fields/fieldValue.interface"; -export const useExtractFields = (fields: Record) => { - const [textInputs, setTextInputs] = useState([]); - const [textAreas, setTextAreas] = useState([]); +export interface ExtractedField { + name: string; + value: string; +} + +export const useExtractFields = (fields?: Record) => { + const [textInputs, setTextInputs] = useState([]); + const [textAreas, setTextAreas] = useState([]); useEffect(() => { - const extractedTextInputs = Object.entries(fields) - .filter(([_, field]) => field.type === "textinput") - .map(([key, field]) => ({ key, ...field })); + if (!fields) { + setTextInputs([]); + setTextAreas([]); + return; + } + + const inputs: ExtractedField[] = []; + const areas: ExtractedField[] = []; - const extractedTextAreas = Object.entries(fields) - .filter(([_, field]) => field.type === "textarea") - .map(([key, field]) => ({ key, ...field })); + for (const [name, field] of Object.entries(fields)) { + if (field.type === "textinput" && field.textinput) { + inputs.push({ name, value: field.textinput.value }); + } else if (field.type === "textarea" && field.textarea) { + areas.push({ name, value: field.textarea.value }); + } + } - setTextInputs(extractedTextInputs.map((field) => field.textinput!.value)); - setTextAreas(extractedTextAreas.map((field) => field.textarea!.value)); + setTextInputs(inputs); + setTextAreas(areas); }, [fields]); return { textInputs, textAreas }; diff --git a/client/src/pages/communities.page.tsx b/client/src/pages/communities.page.tsx index 3bad22f..6abd448 100644 --- a/client/src/pages/communities.page.tsx +++ b/client/src/pages/communities.page.tsx @@ -63,7 +63,6 @@ const CommunitiesPage = () => { useEffect(() => { const timer = setTimeout(() => { if (scrollContainerRef.current) { - console.log(scroll); scrollContainerRef.current.scrollTo({ top: scroll, behavior: "smooth", diff --git a/client/src/pages/community.page.tsx b/client/src/pages/community.page.tsx index e415c3d..27b6fc6 100644 --- a/client/src/pages/community.page.tsx +++ b/client/src/pages/community.page.tsx @@ -57,12 +57,6 @@ const CommunityPage = () => { return () => clearTimeout(timer); }, [isPending]); - useEffect(() => { - if (!isPending) { - setLoadedFirstTime(true); - } - }, [isPending]); - return ( @@ -108,6 +102,7 @@ const CommunityPage = () => { {chatData?.map((user, index) => ( goToProfile(user.user.id)} index={index} key={user.user.id} member={user} diff --git a/client/src/pages/initial.page.tsx b/client/src/pages/initial.page.tsx index 84300a7..ac89426 100644 --- a/client/src/pages/initial.page.tsx +++ b/client/src/pages/initial.page.tsx @@ -5,7 +5,6 @@ import useInitDataStore from "../stores/InitData.store"; const InitialPage = () => { const { initDataStartParam: startParam, initData } = useInitDataStore(); const navigate = useNavigate(); - console.log(initData); useEffect(() => { if ( startParam !== undefined && diff --git a/client/src/pages/profile.page.tsx b/client/src/pages/profile.page.tsx index aec1ed9..ad6c6a5 100644 --- a/client/src/pages/profile.page.tsx +++ b/client/src/pages/profile.page.tsx @@ -8,10 +8,17 @@ import DevImage from "../assets/dev.png"; import ButtonComponent from "../components/button.component"; import TGWhite from "../assets/tg_white.svg"; import useMember from "../hooks/members/fetchHooks/useMember"; +import { useExtractFields } from "../hooks/utils/extractFields"; const ProfilePage = () => { const { communityId, memberId } = useParams(); - const { data } = useMember(communityId ?? "", memberId ?? ""); - const userData = data?.user; + const { data, isLoading } = useMember(communityId ?? "", memberId ?? ""); + + const { textInputs, textAreas } = useExtractFields(data?.config.fields); + + if (isLoading || !data) return null; + + const userData = data.user; + return (
    @@ -44,14 +51,25 @@ const ProfilePage = () => {
    - - + {textInputs.map((field, index) => ( + + ))}
    ПОДРОБНЕЕ
    - + {textAreas.map((field, index) => ( + + ))}
    From e4b1b2f742e0b1690e2fab896330eb5bc7ccfa27 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 8 May 2025 19:25:49 +0300 Subject: [PATCH 21/78] profiles redirection --- client/src/App.tsx | 20 ++++- client/src/components/fields.component.tsx | 41 ++++++++++ client/src/components/profile.component.tsx | 17 ++-- .../hooks/members/search/useSearchMembers.ts | 3 +- client/src/pages/communities.page.tsx | 6 +- client/src/pages/community.page.tsx | 22 +++-- ...e.tsx => communityCurrentProfile.page.tsx} | 35 ++++++-- .../src/pages/generalCurrentProfile.page.tsx | 82 +++++++++++++++++++ client/src/pages/initial.page.tsx | 2 + client/src/stores/user.store.ts | 33 +++----- client/src/utils/profileRedirection.tsx | 20 +++++ client/src/utils/protectedRoute.tsx | 24 ++++-- 12 files changed, 246 insertions(+), 59 deletions(-) create mode 100644 client/src/components/fields.component.tsx rename client/src/pages/{currentProfile.page.tsx => communityCurrentProfile.page.tsx} (66%) create mode 100644 client/src/pages/generalCurrentProfile.page.tsx create mode 100644 client/src/utils/profileRedirection.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 7c843a4..61e07bf 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -8,9 +8,11 @@ import InitDataWrapper from "./utils/InitDataWrapper"; import ProtectedRoute from "./utils/protectedRoute"; import { AboutFirstPage, AboutSecondPage } from "./pages/about.page"; import ChatPage from "./pages/community.page"; -import CurrentProfilePage from "./pages/currentProfile.page"; +import CurrentProfilePage from "./pages/communityCurrentProfile.page"; import ProfilePage from "./pages/profile.page"; import CommunitiesPage from "./pages/communities.page"; +import ProfileRedirection from "./utils/profileRedirection"; +import GeneralCurrentProfilePage from "./pages/generalCurrentProfile.page"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -21,7 +23,6 @@ const pageVariants = { function App() { const navigate = useNavigate(); const location = useLocation(); - const setBackButtonHandler = useCallback(() => { backButton.onClick(() => { navigate(-1); @@ -52,9 +53,20 @@ function App() { } /> } + element={ + + + + } + /> + } + /> + } /> - } /> {/* } /> */} {/* } /> */} diff --git a/client/src/components/fields.component.tsx b/client/src/components/fields.component.tsx new file mode 100644 index 0000000..34afabd --- /dev/null +++ b/client/src/components/fields.component.tsx @@ -0,0 +1,41 @@ +import { useExtractFields } from "../hooks/utils/extractFields"; +import { Community } from "../types/community/community.interface"; +import { FieldValue } from "../types/fields/fieldValue.interface"; +import InfoBlockComponent from "./infoBlock.component"; +import InfoParagraphComponent from "./infoParagraph.component"; + +interface FieldsComponentProps { + fields: Record; + community: Community; +} +const FieldsComponent = (props: FieldsComponentProps) => { + const { textInputs, textAreas } = useExtractFields(props.fields); + return ( +
    +
    + {props.community.name} +
    + + {textInputs.map((field, index) => ( + + ))} + +
    ПОДРОБНЕЕ
    + + {textAreas.map((field, index) => ( + + ))} + +
    + ); +}; + +export default FieldsComponent; diff --git a/client/src/components/profile.component.tsx b/client/src/components/profile.component.tsx index ad3afc0..e5aafc9 100644 --- a/client/src/components/profile.component.tsx +++ b/client/src/components/profile.component.tsx @@ -1,17 +1,16 @@ -import { initData } from "@telegram-apps/sdk-react"; import { handleImageError } from "../utils/imageErrorHandler"; -import { useNavigate } from "react-router-dom"; import DevImage from "../assets/dev.png"; -const ProfileComponent = () => { - const avatar = initData.user()?.photo_url; - const navigate = useNavigate(); - const goToProfile = () => { - navigate("/profile"); - }; +import useUserStore from "../stores/user.store"; + +interface ProfileComponentProps { + onClick?: () => void; +} +const ProfileComponent = (props: ProfileComponentProps) => { + const avatar = useUserStore().userData.avatar; return ( <> -
    +
    { communityId, query, ], + placeholderData: keepPreviousData, }); }; diff --git a/client/src/pages/communities.page.tsx b/client/src/pages/communities.page.tsx index 6abd448..42b7d11 100644 --- a/client/src/pages/communities.page.tsx +++ b/client/src/pages/communities.page.tsx @@ -36,6 +36,10 @@ const CommunitiesPage = () => { navigate(`/community/${communityId}`); }; + const goToMyProfile = () => { + navigate(`/profile/current/`); + }; + const deleteHandler = async (chatPreviewData: Community) => { popup .open({ @@ -97,7 +101,7 @@ const CommunitiesPage = () => { }} />
    - + goToMyProfile()} />
    { navigate(`/community/${communityId}/member/${memberId}`); }; + const goToMyProfile = () => { + navigate(`/profile/current/${communityId}`); + }; + const { data: previewChatData, isSuccess } = useCommunity(communityId ?? ""); const scrollContainerRef = useRef(null); @@ -66,12 +71,17 @@ const CommunityPage = () => { className="max-w-[95%] max-h-[100vh] overflow-auto scroll-container mx-auto px-4" > {isSuccess && ( - +
    + +
    + goToMyProfile()} /> +
    +
    )} { - const { data } = useGetMe(); +const CommunityCurrentProfilePage = () => { + const { communityId } = useParams(); + const { data, isLoading } = useGetMe(); const userData = data?.user; - const navigate = useNavigate(); + const fields = data?.members.find( + (member) => member.community.id === communityId + )?.config.fields; + + const { textInputs, textAreas } = useExtractFields(fields); + const navigate = useNavigate(); const goToEditProfilePage = () => { navigate("/profile/edit"); }; + if (!data || isLoading) return null; return (
    @@ -39,12 +47,23 @@ const CurrentProfilePage = () => {
    - - + {textInputs.map((field, index) => ( + + ))}
    ПОДРОБНЕЕ
    - + {textAreas.map((field, index) => ( + + ))}
    @@ -59,4 +78,4 @@ const CurrentProfilePage = () => { ); }; -export default CurrentProfilePage; +export default CommunityCurrentProfilePage; diff --git a/client/src/pages/generalCurrentProfile.page.tsx b/client/src/pages/generalCurrentProfile.page.tsx new file mode 100644 index 0000000..696673e --- /dev/null +++ b/client/src/pages/generalCurrentProfile.page.tsx @@ -0,0 +1,82 @@ +import { handleImageError } from "../utils/imageErrorHandler"; +import { useNavigate } from "react-router-dom"; +import EBBComponent from "../components/enableBackButtonComponent"; +import DevImage from "../assets/dev.png"; +import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; +import useGetMe from "../hooks/users/fetchHooks/useGetMe"; +import FieldsComponent from "../components/fields.component"; + +const GeneralCurrentProfilePage = () => { + const { data, isLoading } = useGetMe(); + const userData = data?.user; + + const members = data?.members; + + const navigate = useNavigate(); + const goToEditProfilePage = () => { + navigate("/profile/edit"); + }; + + if (!data || isLoading) return null; + return ( + +
    +
    +
    + +
    +

    + {userData?.firstName} {userData?.lastName} +

    +

    + {`@${userData?.telegramUsername}`} +

    +
    +
    +
    + {members?.map((member, index) => ( + + ))} +
    + {/*
    + + {textInputs.map((field, index) => ( + + ))} + +
    ПОДРОБНЕЕ
    + + {textAreas.map((field, index) => ( + + ))} + +
    */} +
    +
    +
    + +
    + ); +}; + +export default GeneralCurrentProfilePage; diff --git a/client/src/pages/initial.page.tsx b/client/src/pages/initial.page.tsx index ac89426..7eb14cf 100644 --- a/client/src/pages/initial.page.tsx +++ b/client/src/pages/initial.page.tsx @@ -5,6 +5,7 @@ import useInitDataStore from "../stores/InitData.store"; const InitialPage = () => { const { initDataStartParam: startParam, initData } = useInitDataStore(); const navigate = useNavigate(); + console.log(initData); useEffect(() => { if ( startParam !== undefined && @@ -17,6 +18,7 @@ const InitialPage = () => { navigate("/communities", { replace: true }); } }, []); + return null; }; diff --git a/client/src/stores/user.store.ts b/client/src/stores/user.store.ts index b6796d5..5fca565 100644 --- a/client/src/stores/user.store.ts +++ b/client/src/stores/user.store.ts @@ -1,32 +1,21 @@ import { create } from "zustand"; -import { UserData } from "../types/user.interface"; +import { User } from "../types/user/user.interface"; interface UserState { - isAuthenticated: boolean; - isLoading: boolean; - userData: Omit; - updateUserData: (userData: Omit) => void; - setIsLoading: (isLoading: boolean) => void; - authenticate: () => void; + userData: User; + setUserData: (data: User) => void; } const useUserStore = create((set) => ({ userData: { - firstName: null, - lastName: null, - company: null, - role: null, - avatar: null, - telegramUsername: null, - bio: null, - id: null, - telegramId: null, + avatar: "", + firstName: "", + lastName: "", + id: "", + telegramId: 0, + telegramUsername: "", }, - isLoading: true, - isAuthenticated: false, - authenticate: () => set(() => ({ isAuthenticated: true })), - updateUserData: (userData: Omit) => - set({ userData }), - setIsLoading: (isLoading: boolean) => set(() => ({ isLoading: isLoading })), + setUserData: (data: User) => set(() => ({ userData: data })), })); + export default useUserStore; diff --git a/client/src/utils/profileRedirection.tsx b/client/src/utils/profileRedirection.tsx new file mode 100644 index 0000000..498e619 --- /dev/null +++ b/client/src/utils/profileRedirection.tsx @@ -0,0 +1,20 @@ +import { ReactNode } from "react"; +import { Navigate, useParams } from "react-router-dom"; +import useUserStore from "../stores/user.store"; + +const ProfileRedirection = ({ children }: { children: ReactNode }) => { + const { communityId, memberId } = useParams(); + const userStore = useUserStore(); + + if ( + memberId && + userStore.userData?.id && + memberId === userStore.userData.id + ) { + return ; + } + + return <>{children}; +}; + +export default ProfileRedirection; diff --git a/client/src/utils/protectedRoute.tsx b/client/src/utils/protectedRoute.tsx index 4913f19..b0b5a7f 100644 --- a/client/src/utils/protectedRoute.tsx +++ b/client/src/utils/protectedRoute.tsx @@ -1,14 +1,22 @@ import { Navigate, Outlet } from "react-router-dom"; +import { useEffect } from "react"; import useGetMe from "../hooks/users/fetchHooks/useGetMe"; +import useUserStore from "../stores/user.store"; + const ProtectedRoute = () => { - const { isPending, data } = useGetMe(); - if (isPending) { - return null; - } - - if (!data) { - return ; - } + const userStore = useUserStore(); + const { isPending, data, isSuccess, isError } = useGetMe(); + + useEffect(() => { + if (isSuccess && data) { + userStore.setUserData(data.user); + } + }, [isSuccess, data]); + + if (isPending) return null; + + if (isError) return ; + return ; }; From e92ba1dbbf64298f594bd455163328fd42046c74 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 9 May 2025 01:10:05 +0300 Subject: [PATCH 22/78] custom forms boilerplate --- client/package-lock.json | 17 ++ client/package.json | 1 + client/src/App.tsx | 4 +- client/src/api/communities.api.ts | 9 +- .../src/components/chatPreview.component.tsx | 2 +- .../requireMembership.component.tsx | 6 +- .../communities/mutations/useJoinCommunity.ts | 9 +- .../pages/communityCurrentProfile.page.tsx | 4 +- client/src/pages/initial.page.tsx | 2 +- client/src/pages/registration.page.tsx | 222 ++---------------- .../types/community/community.interface.ts | 1 + client/src/types/form/form.ts | 10 + client/src/utils/dynamicForm.tsx | 121 ++++++++++ 13 files changed, 196 insertions(+), 212 deletions(-) create mode 100644 client/src/types/form/form.ts create mode 100644 client/src/utils/dynamicForm.tsx diff --git a/client/package-lock.json b/client/package-lock.json index 5618de3..0eacfc8 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -16,6 +16,7 @@ "node": "^23.9.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.56.3", "react-router-dom": "^7.3.0", "shamps-tunnel": "^1.1.3", "tailwindcss": "^4.0.13", @@ -3944,6 +3945,22 @@ "react": "^19.0.0" } }, + "node_modules/react-hook-form": { + "version": "7.56.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.3.tgz", + "integrity": "sha512-IK18V6GVbab4TAo1/cz3kqajxbDPGofdF0w7VHdCo0Nt8PrPlOZcuuDq9YYIV1BtjcX78x0XsldbQRQnQXWXmw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", diff --git a/client/package.json b/client/package.json index 54b199a..435a96a 100644 --- a/client/package.json +++ b/client/package.json @@ -19,6 +19,7 @@ "node": "^23.9.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.56.3", "react-router-dom": "^7.3.0", "shamps-tunnel": "^1.1.3", "tailwindcss": "^4.0.13", diff --git a/client/src/App.tsx b/client/src/App.tsx index 61e07bf..a92f7c5 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -13,6 +13,7 @@ import ProfilePage from "./pages/profile.page"; import CommunitiesPage from "./pages/communities.page"; import ProfileRedirection from "./utils/profileRedirection"; import GeneralCurrentProfilePage from "./pages/generalCurrentProfile.page"; +import RegistrationPage from "./pages/registration.page"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -65,7 +66,8 @@ function App() { /> } + // element={} + element={} /> {/* } /> */} {/* } /> */} diff --git a/client/src/api/communities.api.ts b/client/src/api/communities.api.ts index d023291..274ad38 100644 --- a/client/src/api/communities.api.ts +++ b/client/src/api/communities.api.ts @@ -70,13 +70,14 @@ export const getCommunity = async ( export const joinCommunity = async ( initData: string, - id: string -): Promise => { + id: string, + memberConfig: MemberConfig +): Promise => { try { - const response = await api.post(`/communities/id/${id}`, { + await api.post(`/communities/id/${id}/join`, memberConfig, { headers: { "X-Api-Token": initData }, }); - return response.data; + return null; } catch (error) { console.error("Ошибка при присоединении к сообществу:", error); return null; diff --git a/client/src/components/chatPreview.component.tsx b/client/src/components/chatPreview.component.tsx index 84ebb22..41d7376 100644 --- a/client/src/components/chatPreview.component.tsx +++ b/client/src/components/chatPreview.component.tsx @@ -59,7 +59,7 @@ const ChatPreviewComponent = (

    {props.chatData.name}

    - {props.chatData.members.length} участников + {props.chatData.membersCount} участников

    diff --git a/client/src/components/requireMembership.component.tsx b/client/src/components/requireMembership.component.tsx index 4edc232..de06679 100644 --- a/client/src/components/requireMembership.component.tsx +++ b/client/src/components/requireMembership.component.tsx @@ -1,6 +1,6 @@ import { useNavigate } from "react-router-dom"; import useInitDataStore from "../stores/InitData.store"; -import useCommunity from "../hooks/communities/fetchHooks/useСommunity"; +import useCommunityPreview from "../hooks/communities/fetchHooks/useCommunityPreview"; interface RequireMembershipComponentProps { children: React.ReactNode; @@ -11,13 +11,13 @@ const RequireMembershipComponent = (props: RequireMembershipComponentProps) => { const { initDataStartParam } = useInitDataStore(); const chatID = props.chatID ?? initDataStartParam; - const { isPending, isSuccess } = useCommunity(chatID || ""); + const { isPending, data } = useCommunityPreview(chatID || ""); if (isPending) { return null; } - if (!initDataStartParam || (initDataStartParam === chatID && isSuccess)) { + if (data?.isMember) { return <>{props.children}; } diff --git a/client/src/hooks/communities/mutations/useJoinCommunity.ts b/client/src/hooks/communities/mutations/useJoinCommunity.ts index e936c13..54f877e 100644 --- a/client/src/hooks/communities/mutations/useJoinCommunity.ts +++ b/client/src/hooks/communities/mutations/useJoinCommunity.ts @@ -1,6 +1,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import useInitDataStore from "../../../stores/InitData.store"; import { joinCommunity } from "../../../api/communities.api"; +import { MemberConfig } from "../../../types/member/memberConfig.interface"; const useJoinCommunity = () => { const { initData } = useInitDataStore(); @@ -8,7 +9,13 @@ const useJoinCommunity = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (communityId: string) => joinCommunity(initData, communityId), + mutationFn: ({ + communityId, + memberConfig, + }: { + communityId: string; + memberConfig: MemberConfig; + }) => joinCommunity(initData, communityId, memberConfig), onSuccess: (_, communityId) => { queryClient.invalidateQueries({ queryKey: ["/communities", initData] }); queryClient.invalidateQueries({ diff --git a/client/src/pages/communityCurrentProfile.page.tsx b/client/src/pages/communityCurrentProfile.page.tsx index 165330a..e0cd716 100644 --- a/client/src/pages/communityCurrentProfile.page.tsx +++ b/client/src/pages/communityCurrentProfile.page.tsx @@ -11,7 +11,6 @@ import { useExtractFields } from "../hooks/utils/extractFields"; const CommunityCurrentProfilePage = () => { const { communityId } = useParams(); const { data, isLoading } = useGetMe(); - const userData = data?.user; const fields = data?.members.find( (member) => member.community.id === communityId @@ -25,6 +24,9 @@ const CommunityCurrentProfilePage = () => { }; if (!data || isLoading) return null; + + const userData = data?.user; + console.log(userData.avatar); return (
    diff --git a/client/src/pages/initial.page.tsx b/client/src/pages/initial.page.tsx index 7eb14cf..ece6954 100644 --- a/client/src/pages/initial.page.tsx +++ b/client/src/pages/initial.page.tsx @@ -13,7 +13,7 @@ const InitialPage = () => { startParam.length > 0 ) { navigate("/communities", { replace: true }); - navigate(`/chat/${startParam}`); + // navigate(`/chat/${startParam}`); } else { navigate("/communities", { replace: true }); } diff --git a/client/src/pages/registration.page.tsx b/client/src/pages/registration.page.tsx index a9adb91..1d2e0bc 100644 --- a/client/src/pages/registration.page.tsx +++ b/client/src/pages/registration.page.tsx @@ -1,205 +1,27 @@ -// import { useState, useEffect } from "react"; -// import InputFieldComponent from "../components/inputField.component"; -// import TextareaFieldComponent from "../components/textareaField.component"; -// import { handleImageError } from "../utils/imageErrorHandler"; -// import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; -// import EBBComponent from "../components/enableBackButtonComponent"; -// import { useNavigate } from "react-router-dom"; -// import DevImage from "../assets/dev.png"; -// import ButtonComponent from "../components/button.component"; -// import useStartParamStore from "../stores/StartData.store"; -// import useInitDataStore from "../stores/InitData.store"; - -// const RegistrationPage = () => { -// //mutations -// const createMeMutation = useCreateMe(); -// const joinMeMutation = useJoinMe(); - -// const navigate = useNavigate(); - -// //tg params -// const { startParam } = useStartParamStore(); -// const { initDataUser, launchParams } = useInitDataStore(); -// const avatar = initDataUser?.photo_url; - -// //form validation data -// const [profileData, setProfileData] = useState< -// Pick -// >({ -// firstName: initDataUser?.first_name || "", -// lastName: initDataUser?.last_name || "", -// company: "", -// role: "", -// bio: "", -// }); -// const [isFilled, setIsFilled] = useState(false); -// const isProfileFilled = () => { -// const isFilled = -// profileData.firstName?.trim() != "" && -// profileData.lastName?.trim() !== "" && -// profileData.company?.trim() !== "" && -// profileData.role?.trim() !== "" && -// profileData.bio?.trim() !== ""; - -// return isFilled; -// }; - -// const MAX_INPUT_LENGTH = 40; -// const MAX_TEXTAREA_LENGTH = 230; - -// const handleInputChange = (field: keyof UserData, value: string) => { -// setProfileData((prevState) => ({ -// ...prevState, -// [field]: value, -// })); -// }; - -// const handleRegister = async () => { -// try { -// await createMeMutation.mutateAsync(profileData); -// navigate("/chats", { replace: true }); -// if (startParam !== undefined && startParam.length > 0) { -// await joinMeMutation.mutateAsync(startParam); -// navigate(`/chat/${startParam}`); -// } else { -// navigate("/chats", { replace: true }); -// } -// } catch (error) { -// console.error("Ошибка при создании пользователя", error); -// } -// }; - -// const { data: preview } = useMePreview(); -// useEffect(() => { -// if (preview?.bio) { -// setProfileData((prevState) => ({ -// ...prevState, -// bio: preview.bio, -// })); -// } -// }, [preview?.bio]); - -// useEffect(() => { -// setIsFilled(isProfileFilled()); -// }, [profileData]); - -// const [iosKeyboardOpen, setIosKeyboardOpen] = useState(false); -// const [focusedFieldsCount, setFocusedFiledsCount] = useState(0); -// const onFocusHandler = () => { -// setFocusedFiledsCount(focusedFieldsCount + 1); -// }; -// const onBlurHandler = () => { -// setFocusedFiledsCount(focusedFieldsCount - 1); -// }; -// const handleFieldsCount = () => { -// if (launchParams?.tgWebAppPlatform === "ios") { -// if (focusedFieldsCount > 0) { -// setIosKeyboardOpen(true); -// } -// if (focusedFieldsCount === 0) { -// setIosKeyboardOpen(false); -// } -// } -// }; -// useEffect(handleFieldsCount, [focusedFieldsCount]); -// return ( -// -//
    -//
    -// -//
    -//
    -// handleInputChange("firstName", value)} -// value={profileData.firstName ?? ""} -// maxLength={MAX_INPUT_LENGTH} -// /> -// handleInputChange("lastName", value)} -// value={profileData.lastName ?? ""} -// maxLength={MAX_INPUT_LENGTH} -// /> -// handleInputChange("company", value)} -// value={profileData.company ?? ""} -// maxLength={MAX_INPUT_LENGTH} -// /> -// handleInputChange("role", value)} -// value={profileData.role ?? ""} -// maxLength={MAX_INPUT_LENGTH} -// /> -// handleInputChange("bio", value)} -// title="Описание" -// value={profileData.bio ?? ""} -// maxLength={MAX_TEXTAREA_LENGTH} -// /> -//
    - -// {launchParams?.tgWebAppPlatform !== "ios" && ( -//
    -// )} -// {launchParams?.tgWebAppPlatform === "ios" && ( -//
    { -// if (isFilled) handleRegister(); -// }} -// className="flex w-full justify-center pt-[20px]" -// > -// { -// if (isFilled) handleRegister(); -// }} -// state={isFilled ? "active" : "disabled"} -// /> -//
    -// )} -//
    - -// {launchParams?.tgWebAppPlatform !== "ios" && ( -//
    -// { -// if (isFilled) handleRegister(); -// }} -// state={isFilled ? "active" : "disabled"} -// /> -//
    -// )} -//
    -// ); -// }; -// export default RegistrationPage; +import useCommunityPreview from "../hooks/communities/fetchHooks/useCommunityPreview"; +import useInitDataStore from "../stores/InitData.store"; +import { MemberConfig } from "../types/member/memberConfig.interface"; +import DynamicForm from "../utils/dynamicForm"; const RegistrationPage = () => { - return <>registration; + const { initDataUser } = useInitDataStore(); + const { initDataStartParam: chatId } = useInitDataStore(); + const { data, isPending } = useCommunityPreview(chatId ?? ""); + const fields = data?.config.fields; + + const handleSubmit = async (data: MemberConfig) => { + console.log("Данные формы:", data); + alert(JSON.stringify(data)); + }; + + if (isPending) return null; + return ( + + ); }; export default RegistrationPage; diff --git a/client/src/types/community/community.interface.ts b/client/src/types/community/community.interface.ts index f57136c..99a64cd 100644 --- a/client/src/types/community/community.interface.ts +++ b/client/src/types/community/community.interface.ts @@ -4,6 +4,7 @@ import { CommunityConfig } from "./communityConfig.interface"; export interface Community { avatar: string; config: CommunityConfig; + isMember: boolean; description: string; id: string; membersCount: number; diff --git a/client/src/types/form/form.ts b/client/src/types/form/form.ts new file mode 100644 index 0000000..be9b12f --- /dev/null +++ b/client/src/types/form/form.ts @@ -0,0 +1,10 @@ +import { FieldType } from "../fields/field.type"; + +export type FieldSchema = { + name: string; + label: string; + type: FieldType; + required?: boolean; + maxLength?: number; + componentProps?: Record; +}; diff --git a/client/src/utils/dynamicForm.tsx b/client/src/utils/dynamicForm.tsx new file mode 100644 index 0000000..73ea15e --- /dev/null +++ b/client/src/utils/dynamicForm.tsx @@ -0,0 +1,121 @@ +import { useForm, Controller } from "react-hook-form"; +import DevImage from "../assets/notFound.svg"; +import { Field } from "../types/fields/field.interface"; +import { FieldValue } from "../types/fields/fieldValue.interface"; +import ButtonComponent from "../components/button.component"; +import EBBComponent from "../components/enableBackButtonComponent"; +import InputFieldComponent from "../components/inputField.component"; +import TextareaFieldComponent from "../components/textareaField.component"; +import useInitDataStore from "../stores/InitData.store"; +import { handleImageError } from "./imageErrorHandler"; +import { MemberConfig } from "../types/member/memberConfig.interface"; + +type FormData = Record; + +interface DynamicFormProps { + fields: Field[]; + onSubmit: (data: MemberConfig) => Promise; + avatar?: string; +} + +const DynamicForm = ({ fields, onSubmit, avatar }: DynamicFormProps) => { + const { initDataUser } = useInitDataStore(); + + const { + control, + handleSubmit, + formState: { isValid }, + } = useForm({ + mode: "onChange", + defaultValues: fields.reduce( + (acc, field) => ({ + ...acc, + [field.title]: + field.type === "textarea" + ? field.textarea?.default || "" + : field.textinput?.default || "", + }), + {} as FormData + ), + }); + + const handleFormSubmit = async (data: FormData) => { + try { + const submitData: MemberConfig = { + fields: fields.reduce>((acc, field) => { + acc[field.title] = { + type: field.type, + [field.type]: { + value: data[field.title], + }, + }; + return acc; + }, {}), + }; + + await onSubmit(submitData); + } catch (error) { + console.error("Form submission error:", error); + } + }; + + return ( + +
    +
    + +
    + +
    + {fields.map((field) => ( + + field.type === "textarea" ? ( + + ) : ( + + ) + } + /> + ))} + +
    + +
    + +
    +
    + ); +}; + +export default DynamicForm; From d4daf0f0e9d3813d4a04ea226e36efae3dc3aab1 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 9 May 2025 02:31:52 +0300 Subject: [PATCH 23/78] registration and api fixes --- client/src/App.tsx | 11 +- client/src/api/communities.api.ts | 12 ++- .../requireMembership.component.tsx | 16 +-- client/src/example.json | 7 ++ .../mutations/useLeaveCommunity.ts | 6 +- client/src/pages/initial.page.tsx | 2 +- client/src/pages/invitation.page.tsx | 102 +++++++++--------- client/src/pages/registration.page.tsx | 15 ++- client/src/utils/dynamicForm.tsx | 7 +- 9 files changed, 102 insertions(+), 76 deletions(-) create mode 100644 client/src/example.json diff --git a/client/src/App.tsx b/client/src/App.tsx index a92f7c5..63a34be 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -14,6 +14,7 @@ import CommunitiesPage from "./pages/communities.page"; import ProfileRedirection from "./utils/profileRedirection"; import GeneralCurrentProfilePage from "./pages/generalCurrentProfile.page"; import RegistrationPage from "./pages/registration.page"; +import InvitationPage from "./pages/invitation.page"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -66,16 +67,18 @@ function App() { /> } - element={} + element={} /> {/* } /> */} - {/* } /> */} + } /> } /> } /> } /> - {/* } /> */} + } + /> diff --git a/client/src/api/communities.api.ts b/client/src/api/communities.api.ts index 274ad38..cca5afc 100644 --- a/client/src/api/communities.api.ts +++ b/client/src/api/communities.api.ts @@ -89,10 +89,14 @@ export const leaveCommunity = async ( id: string ): Promise => { try { - const response = await api.post(`/communities/id/${id}`, { - headers: { "X-Api-Token": initData }, - }); - return response.data; + await api.post( + `/communities/id/${id}/leave`, + {}, + { + headers: { "X-Api-Token": initData }, + } + ); + return null; } catch (error) { console.error("Ошибка при выходе из сообщества:", error); return null; diff --git a/client/src/components/requireMembership.component.tsx b/client/src/components/requireMembership.component.tsx index de06679..17df631 100644 --- a/client/src/components/requireMembership.component.tsx +++ b/client/src/components/requireMembership.component.tsx @@ -1,3 +1,4 @@ +import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; import useInitDataStore from "../stores/InitData.store"; import useCommunityPreview from "../hooks/communities/fetchHooks/useCommunityPreview"; @@ -6,6 +7,7 @@ interface RequireMembershipComponentProps { children: React.ReactNode; chatID?: string; } + const RequireMembershipComponent = (props: RequireMembershipComponentProps) => { const navigate = useNavigate(); const { initDataStartParam } = useInitDataStore(); @@ -13,15 +15,17 @@ const RequireMembershipComponent = (props: RequireMembershipComponentProps) => { const chatID = props.chatID ?? initDataStartParam; const { isPending, data } = useCommunityPreview(chatID || ""); - if (isPending) { - return null; - } + useEffect(() => { + if (!isPending && data && !data.isMember) { + navigate("/invite"); + } + }, [isPending, data, navigate]); - if (data?.isMember) { - return <>{props.children}; + if (isPending || (data && !data.isMember)) { + return null; } - navigate("/invite"); + return <>{props.children}; }; export default RequireMembershipComponent; diff --git a/client/src/example.json b/client/src/example.json new file mode 100644 index 0000000..301d915 --- /dev/null +++ b/client/src/example.json @@ -0,0 +1,7 @@ +{ + "fields": { + "Место работы": { "type": "textinput", "textinput": { "value": "fdsfs" } }, + "Должность": { "type": "textinput", "textinput": { "value": "fdsf" } }, + "Описание": { "type": "textarea", "textarea": { "value": "fdsf" } } + } +} diff --git a/client/src/hooks/communities/mutations/useLeaveCommunity.ts b/client/src/hooks/communities/mutations/useLeaveCommunity.ts index 6af3ce2..23728fb 100644 --- a/client/src/hooks/communities/mutations/useLeaveCommunity.ts +++ b/client/src/hooks/communities/mutations/useLeaveCommunity.ts @@ -13,18 +13,18 @@ const useLeaveCommunity = () => { onSuccess: (_, communityId) => { queryClient.setQueryData( ["/communities", initData], - (old) => old?.filter((community) => community.id !== communityId) ?? [], + (old) => old?.filter((community) => community.id !== communityId) ?? [] ); queryClient.setQueriesData( { queryKey: ["/communities", initData] }, (old: Community[] | undefined) => - old?.filter((community) => community.id !== communityId) ?? [], + old?.filter((community) => community.id !== communityId) ?? [] ); queryClient.setQueryData( [`/communities/${communityId}`, initData, communityId], - null, + null ); }, }); diff --git a/client/src/pages/initial.page.tsx b/client/src/pages/initial.page.tsx index ece6954..8edf9cf 100644 --- a/client/src/pages/initial.page.tsx +++ b/client/src/pages/initial.page.tsx @@ -13,7 +13,7 @@ const InitialPage = () => { startParam.length > 0 ) { navigate("/communities", { replace: true }); - // navigate(`/chat/${startParam}`); + navigate(`/community/${startParam}`); } else { navigate("/communities", { replace: true }); } diff --git a/client/src/pages/invitation.page.tsx b/client/src/pages/invitation.page.tsx index 1f67cb1..2aa0087 100644 --- a/client/src/pages/invitation.page.tsx +++ b/client/src/pages/invitation.page.tsx @@ -1,61 +1,57 @@ -// import { handleImageError } from "../utils/imageErrorHandler"; -// import EBBComponent from "../components/enableBackButtonComponent"; -// import { useNavigate } from "react-router-dom"; -// import DevImage from "../assets/dev.png"; -// import useInitDataStore from "../stores/InitData.store"; -// const InvitationPage = () => { -// const navigate = useNavigate(); -// const { initDataStartParam: chatId } = useInitDataStore(); +import { handleImageError } from "../utils/imageErrorHandler"; +import EBBComponent from "../components/enableBackButtonComponent"; +import { useNavigate } from "react-router-dom"; +import DevImage from "../assets/dev.png"; +import useInitDataStore from "../stores/InitData.store"; +import useCommunityPreview from "../hooks/communities/fetchHooks/useCommunityPreview"; -// const joinMeMutation = useJoinMe(); -// const handleAcceptInvitation = async () => { -// await joinMeMutation.mutateAsync(chatId ?? ""); -// navigate("/chats", { replace: true }); -// navigate(`/chat/${chatId}`); -// }; +const InvitationPage = () => { + const navigate = useNavigate(); + const { initDataStartParam: communityId } = useInitDataStore(); -// const { data: chatData } = useChatPreview(chatId ?? ""); + const handleAcceptInvitation = () => { + navigate(`/registration/${communityId}`, { replace: true }); + }; -// return ( -// -//
    -//
    -//

    Расскажите о себе!

    -//

    -// Участники этого чата хотят узнать о вас больше -//

    -//
    + const { data: chatData } = useCommunityPreview(communityId ?? ""); -//
    -// -//
    -//

    -// {chatData?.name ?? "название чата"} -//

    -//

    -// Число участников: {chatData?.usersAmount} -//

    -//
    -//
    + return ( + +
    +
    +

    Расскажите о себе!

    +

    + Участники этого чата хотят узнать о вас больше +

    +
    -//
    -// -//
    -//
    -//
    -// ); -// }; +
    + +
    +

    + {chatData?.name ?? "название чата"} +

    +

    + Число участников: {chatData?.membersCount} +

    +
    +
    -// export default InvitationPage; +
    + +
    +
    +
    + ); +}; -const InvitationPage = () => <>Invitaton page; export default InvitationPage; diff --git a/client/src/pages/registration.page.tsx b/client/src/pages/registration.page.tsx index 1d2e0bc..9960ffa 100644 --- a/client/src/pages/registration.page.tsx +++ b/client/src/pages/registration.page.tsx @@ -1,20 +1,29 @@ +import { useNavigate, useParams } from "react-router-dom"; import useCommunityPreview from "../hooks/communities/fetchHooks/useCommunityPreview"; import useInitDataStore from "../stores/InitData.store"; import { MemberConfig } from "../types/member/memberConfig.interface"; import DynamicForm from "../utils/dynamicForm"; +import useJoinCommunity from "../hooks/communities/mutations/useJoinCommunity"; const RegistrationPage = () => { + const navigate = useNavigate(); + const { communityId } = useParams(); const { initDataUser } = useInitDataStore(); const { initDataStartParam: chatId } = useInitDataStore(); const { data, isPending } = useCommunityPreview(chatId ?? ""); const fields = data?.config.fields; - + const useJoinCommunityMutation = useJoinCommunity(); const handleSubmit = async (data: MemberConfig) => { - console.log("Данные формы:", data); - alert(JSON.stringify(data)); + await useJoinCommunityMutation.mutateAsync({ + communityId: communityId!, + memberConfig: data, + }); + navigate(`/communities`, { replace: true }); + navigate(`/community/${communityId}`); }; if (isPending) return null; + return ( { />
    -
    + {fields.map((field) => ( {
    {}} />
    From 0bdf577ba2644133d8c9e81f3f00c9877032932c Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 9 May 2025 03:11:33 +0300 Subject: [PATCH 24/78] registration redirection fixes (thank god, i've done it...) --- client/src/App.tsx | 8 ++++---- .../src/components/requireMembership.component.tsx | 8 +++++--- .../communities/fetchHooks/useCommunitiesAll.ts | 4 ++-- .../hooks/communities/mutations/useJoinCommunity.ts | 12 +++++++----- client/src/pages/registration.page.tsx | 3 +-- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 63a34be..c46bc87 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -71,14 +71,14 @@ function App() { /> {/* } /> */} } /> + } + /> } /> } /> } /> - } - />
    diff --git a/client/src/components/requireMembership.component.tsx b/client/src/components/requireMembership.component.tsx index 17df631..cf923fd 100644 --- a/client/src/components/requireMembership.component.tsx +++ b/client/src/components/requireMembership.component.tsx @@ -13,13 +13,15 @@ const RequireMembershipComponent = (props: RequireMembershipComponentProps) => { const { initDataStartParam } = useInitDataStore(); const chatID = props.chatID ?? initDataStartParam; - const { isPending, data } = useCommunityPreview(chatID || ""); + const { isSuccess, isPending, data, isRefetching } = useCommunityPreview( + chatID || "" + ); useEffect(() => { - if (!isPending && data && !data.isMember) { + if (!isRefetching && isSuccess && !data?.isMember) { navigate("/invite"); } - }, [isPending, data, navigate]); + }, [isSuccess, data]); if (isPending || (data && !data.isMember)) { return null; diff --git a/client/src/hooks/communities/fetchHooks/useCommunitiesAll.ts b/client/src/hooks/communities/fetchHooks/useCommunitiesAll.ts index 3ac011f..712046b 100644 --- a/client/src/hooks/communities/fetchHooks/useCommunitiesAll.ts +++ b/client/src/hooks/communities/fetchHooks/useCommunitiesAll.ts @@ -13,8 +13,8 @@ const useCommunititesAll = () => { communities?.forEach((community: Community) => { queryClient.setQueryData( - [`/communities/${community.id}`, initData, community.id], - community, + [`/communities/${community.id}`, initData], + community ); }); diff --git a/client/src/hooks/communities/mutations/useJoinCommunity.ts b/client/src/hooks/communities/mutations/useJoinCommunity.ts index 54f877e..01e262f 100644 --- a/client/src/hooks/communities/mutations/useJoinCommunity.ts +++ b/client/src/hooks/communities/mutations/useJoinCommunity.ts @@ -16,11 +16,13 @@ const useJoinCommunity = () => { communityId: string; memberConfig: MemberConfig; }) => joinCommunity(initData, communityId, memberConfig), - onSuccess: (_, communityId) => { - queryClient.invalidateQueries({ queryKey: ["/communities", initData] }); - queryClient.invalidateQueries({ - queryKey: [`/communities/${communityId}`, initData], - }); + onSuccess: async (_, communityId) => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["/communities", initData] }), + queryClient.refetchQueries({ + queryKey: [`/communities/${communityId}`, initData], + }), + ]); }, }); }; diff --git a/client/src/pages/registration.page.tsx b/client/src/pages/registration.page.tsx index 9960ffa..a339840 100644 --- a/client/src/pages/registration.page.tsx +++ b/client/src/pages/registration.page.tsx @@ -18,8 +18,7 @@ const RegistrationPage = () => { communityId: communityId!, memberConfig: data, }); - navigate(`/communities`, { replace: true }); - navigate(`/community/${communityId}`); + navigate("/communities"); }; if (isPending) return null; From dbb74eedc776765f10180aade64654fcaf53e19c Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 9 May 2025 03:47:28 +0300 Subject: [PATCH 25/78] profile editing in community --- client/src/App.tsx | 5 +++ .../hooks/utils/communityProfileEditData.tsx | 33 +++++++++++++++++++ .../pages/communityCurrentProfile.page.tsx | 2 +- .../src/pages/editProfileCommunity.page.tsx | 32 ++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 client/src/hooks/utils/communityProfileEditData.tsx create mode 100644 client/src/pages/editProfileCommunity.page.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index c46bc87..9df50eb 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -15,6 +15,7 @@ import ProfileRedirection from "./utils/profileRedirection"; import GeneralCurrentProfilePage from "./pages/generalCurrentProfile.page"; import RegistrationPage from "./pages/registration.page"; import InvitationPage from "./pages/invitation.page"; +import EditProfileCommunityPage from "./pages/editProfileCommunity.page"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -65,6 +66,10 @@ function App() { path="/profile/current/:communityId" element={} /> + } + /> } diff --git a/client/src/hooks/utils/communityProfileEditData.tsx b/client/src/hooks/utils/communityProfileEditData.tsx new file mode 100644 index 0000000..fe91444 --- /dev/null +++ b/client/src/hooks/utils/communityProfileEditData.tsx @@ -0,0 +1,33 @@ +import useCommunityPreview from "../communities/fetchHooks/useCommunityPreview"; +import useGetMe from "../users/fetchHooks/useGetMe"; + +export const useCommunityProfileEditData = ( + communityId: string | undefined +) => { + const { data: previewData, isPending: isPreviewPending } = + useCommunityPreview(communityId ?? ""); + + const { data: userData, isPending: isUserPending } = useGetMe(); + + const member = userData?.members.find( + (member) => member.community.id === communityId + ); + + const fields = previewData?.config.fields.map((field) => { + const value = member?.config.fields[field.title]?.[field.type]?.value ?? ""; + + return { + ...field, + [field.type]: { + ...field[field.type], + default: value, + }, + }; + }); + + return { + isPending: isPreviewPending || isUserPending, + avatar: userData?.user.avatar, + fields, + }; +}; diff --git a/client/src/pages/communityCurrentProfile.page.tsx b/client/src/pages/communityCurrentProfile.page.tsx index e0cd716..29273f3 100644 --- a/client/src/pages/communityCurrentProfile.page.tsx +++ b/client/src/pages/communityCurrentProfile.page.tsx @@ -20,7 +20,7 @@ const CommunityCurrentProfilePage = () => { const navigate = useNavigate(); const goToEditProfilePage = () => { - navigate("/profile/edit"); + navigate(`/profile/current/${communityId}/edit`); }; if (!data || isLoading) return null; diff --git a/client/src/pages/editProfileCommunity.page.tsx b/client/src/pages/editProfileCommunity.page.tsx new file mode 100644 index 0000000..12914f8 --- /dev/null +++ b/client/src/pages/editProfileCommunity.page.tsx @@ -0,0 +1,32 @@ +import { useNavigate, useParams } from "react-router-dom"; +import useCommunityPreview from "../hooks/communities/fetchHooks/useCommunityPreview"; +import useJoinCommunity from "../hooks/communities/mutations/useJoinCommunity"; +import { MemberConfig } from "../types/member/memberConfig.interface"; +import DynamicForm from "../utils/dynamicForm"; +import { useCommunityProfileEditData } from "../hooks/utils/communityProfileEditData"; + +const EditProfileCommunityPage = () => { + const navigate = useNavigate(); + const { communityId } = useParams(); + // const { data } = useCommunityPreview(communityId ?? ""); + + const { fields, isPending, avatar } = + useCommunityProfileEditData(communityId); + + const handleSubmit = async (data: MemberConfig) => { + alert("АХХАХАХАХХАХА А РУЧКИ ТО НЕТУ))))"); + navigate("/communities"); + }; + + if (isPending) return null; + + return ( + + ); +}; + +export default EditProfileCommunityPage; From 6dccee5ed59eaad64763317c1aa729fd83d0ae02 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 9 May 2025 03:51:56 +0300 Subject: [PATCH 26/78] just a commit --- client/src/pages/editProfileCommunity.page.tsx | 2 -- client/src/pages/generalCurrentProfile.page.tsx | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/client/src/pages/editProfileCommunity.page.tsx b/client/src/pages/editProfileCommunity.page.tsx index 12914f8..13d60eb 100644 --- a/client/src/pages/editProfileCommunity.page.tsx +++ b/client/src/pages/editProfileCommunity.page.tsx @@ -1,6 +1,4 @@ import { useNavigate, useParams } from "react-router-dom"; -import useCommunityPreview from "../hooks/communities/fetchHooks/useCommunityPreview"; -import useJoinCommunity from "../hooks/communities/mutations/useJoinCommunity"; import { MemberConfig } from "../types/member/memberConfig.interface"; import DynamicForm from "../utils/dynamicForm"; import { useCommunityProfileEditData } from "../hooks/utils/communityProfileEditData"; diff --git a/client/src/pages/generalCurrentProfile.page.tsx b/client/src/pages/generalCurrentProfile.page.tsx index 696673e..13a444a 100644 --- a/client/src/pages/generalCurrentProfile.page.tsx +++ b/client/src/pages/generalCurrentProfile.page.tsx @@ -70,11 +70,11 @@ const GeneralCurrentProfilePage = () => {
    - + /> */} ); }; From cc53d2f5b8176466240d30debb5cd8a5458ae688 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 9 May 2025 03:55:52 +0300 Subject: [PATCH 27/78] fix lint --- client/src/pages/editProfileCommunity.page.tsx | 3 +-- client/src/pages/generalCurrentProfile.page.tsx | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/client/src/pages/editProfileCommunity.page.tsx b/client/src/pages/editProfileCommunity.page.tsx index 13d60eb..057cb6c 100644 --- a/client/src/pages/editProfileCommunity.page.tsx +++ b/client/src/pages/editProfileCommunity.page.tsx @@ -1,5 +1,4 @@ import { useNavigate, useParams } from "react-router-dom"; -import { MemberConfig } from "../types/member/memberConfig.interface"; import DynamicForm from "../utils/dynamicForm"; import { useCommunityProfileEditData } from "../hooks/utils/communityProfileEditData"; @@ -11,7 +10,7 @@ const EditProfileCommunityPage = () => { const { fields, isPending, avatar } = useCommunityProfileEditData(communityId); - const handleSubmit = async (data: MemberConfig) => { + const handleSubmit = async () => { alert("АХХАХАХАХХАХА А РУЧКИ ТО НЕТУ))))"); navigate("/communities"); }; diff --git a/client/src/pages/generalCurrentProfile.page.tsx b/client/src/pages/generalCurrentProfile.page.tsx index 13a444a..4f40117 100644 --- a/client/src/pages/generalCurrentProfile.page.tsx +++ b/client/src/pages/generalCurrentProfile.page.tsx @@ -1,8 +1,8 @@ import { handleImageError } from "../utils/imageErrorHandler"; -import { useNavigate } from "react-router-dom"; +// import { useNavigate } from "react-router-dom"; import EBBComponent from "../components/enableBackButtonComponent"; import DevImage from "../assets/dev.png"; -import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; +// import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; import useGetMe from "../hooks/users/fetchHooks/useGetMe"; import FieldsComponent from "../components/fields.component"; @@ -12,10 +12,10 @@ const GeneralCurrentProfilePage = () => { const members = data?.members; - const navigate = useNavigate(); - const goToEditProfilePage = () => { - navigate("/profile/edit"); - }; + // const navigate = useNavigate(); + // const goToEditProfilePage = () => { + // navigate("/profile/edit"); + // }; if (!data || isLoading) return null; return ( From 537c17a8cef1c7385b878f7113c8ebb8a72198be Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 9 May 2025 12:27:57 +0300 Subject: [PATCH 28/78] editing in general profile base functionality --- client/src/components/editCard.component.tsx | 20 ++++++++++++++++ client/src/components/fields.component.tsx | 9 +++---- .../src/components/profileCard.component.tsx | 24 +++++++++++++++++++ .../components/profileCardInfo.component.tsx | 0 .../src/pages/generalCurrentProfile.page.tsx | 4 ++-- client/src/utils/dynamicForm.tsx | 15 +----------- 6 files changed, 50 insertions(+), 22 deletions(-) create mode 100644 client/src/components/editCard.component.tsx create mode 100644 client/src/components/profileCard.component.tsx create mode 100644 client/src/components/profileCardInfo.component.tsx diff --git a/client/src/components/editCard.component.tsx b/client/src/components/editCard.component.tsx new file mode 100644 index 0000000..c164ed5 --- /dev/null +++ b/client/src/components/editCard.component.tsx @@ -0,0 +1,20 @@ +import useCommunityPreview from "../hooks/communities/fetchHooks/useCommunityPreview"; +import DynamicForm from "../utils/dynamicForm"; + +export interface EditCardComponentProps { + communityId: string; +} + +const EditCardComponent = (props: EditCardComponentProps) => { + const { data, isPending } = useCommunityPreview(props.communityId ?? ""); + const fields = data?.config.fields; + const handleSubmit = async () => { + alert("АХАХХАХАХАХ А РУЧКИ ТО НЕТУ)))))"); + }; + + if (isPending) return null; + + return ; +}; + +export default EditCardComponent; diff --git a/client/src/components/fields.component.tsx b/client/src/components/fields.component.tsx index 34afabd..e3d74e3 100644 --- a/client/src/components/fields.component.tsx +++ b/client/src/components/fields.component.tsx @@ -4,17 +4,14 @@ import { FieldValue } from "../types/fields/fieldValue.interface"; import InfoBlockComponent from "./infoBlock.component"; import InfoParagraphComponent from "./infoParagraph.component"; -interface FieldsComponentProps { +export interface FieldsComponentProps { fields: Record; community: Community; } const FieldsComponent = (props: FieldsComponentProps) => { const { textInputs, textAreas } = useExtractFields(props.fields); return ( -
    -
    - {props.community.name} -
    + <> {textInputs.map((field, index) => ( { /> ))} -
    + ); }; diff --git a/client/src/components/profileCard.component.tsx b/client/src/components/profileCard.component.tsx new file mode 100644 index 0000000..6a22fdf --- /dev/null +++ b/client/src/components/profileCard.component.tsx @@ -0,0 +1,24 @@ +import { useState } from "react"; +import FieldsComponent, { FieldsComponentProps } from "./fields.component"; +import ButtonComponent from "./button.component"; +import EditCardComponent from "./editCard.component"; + +const ProfileCardComponent = (props: FieldsComponentProps) => { + const [isEditing, setIsEditing] = useState(false); + return ( +
    +
    + {props.community.name} + setIsEditing(!isEditing)} + state={"active"} + /> +
    + {!isEditing && } + {isEditing && } +
    + ); +}; + +export default ProfileCardComponent; diff --git a/client/src/components/profileCardInfo.component.tsx b/client/src/components/profileCardInfo.component.tsx new file mode 100644 index 0000000..e69de29 diff --git a/client/src/pages/generalCurrentProfile.page.tsx b/client/src/pages/generalCurrentProfile.page.tsx index 4f40117..6ffd3b1 100644 --- a/client/src/pages/generalCurrentProfile.page.tsx +++ b/client/src/pages/generalCurrentProfile.page.tsx @@ -4,7 +4,7 @@ import EBBComponent from "../components/enableBackButtonComponent"; import DevImage from "../assets/dev.png"; // import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; import useGetMe from "../hooks/users/fetchHooks/useGetMe"; -import FieldsComponent from "../components/fields.component"; +import ProfileCardComponent from "../components/profileCard.component"; const GeneralCurrentProfilePage = () => { const { data, isLoading } = useGetMe(); @@ -39,7 +39,7 @@ const GeneralCurrentProfilePage = () => {
    {members?.map((member, index) => ( - ; @@ -18,9 +15,7 @@ interface DynamicFormProps { avatar?: string; } -const DynamicForm = ({ fields, onSubmit, avatar }: DynamicFormProps) => { - const { initDataUser } = useInitDataStore(); - +const DynamicForm = ({ fields, onSubmit }: DynamicFormProps) => { const { control, handleSubmit, @@ -62,14 +57,6 @@ const DynamicForm = ({ fields, onSubmit, avatar }: DynamicFormProps) => { return (
    -
    - -
    -
    Date: Thu, 15 May 2025 00:54:55 +0300 Subject: [PATCH 29/78] edit profile in community --- client/package-lock.json | 55 +++++++ client/package.json | 2 + client/src/api/communities.api.ts | 22 +++ client/src/components/editCard.component.tsx | 13 +- client/src/components/editProfileForm.tsx | 96 ++++++++++++ .../{ => form}/inputField.component.tsx | 0 .../{ => form}/textareaField.component.tsx | 0 .../hooks/members/mutations/usePatchMember.ts | 26 ++++ client/src/hooks/utils/extractFields.ts | 26 ++-- client/src/mappers/FieldValues.ts | 54 +++++++ .../pages/communityCurrentProfile.page.tsx | 35 +++-- .../src/pages/editProfileCommunity.page.tsx | 139 ++++++++++++++++-- client/src/pages/profile.page.tsx | 21 ++- client/src/pages/registration.page.tsx | 54 ++++--- .../src/types/member/patchMember.interface.ts | 8 + client/src/utils/chunkArray.ts | 7 + client/src/utils/dynamicForm.tsx | 111 -------------- client/src/utils/equalFields.ts | 14 ++ 18 files changed, 495 insertions(+), 188 deletions(-) create mode 100644 client/src/components/editProfileForm.tsx rename client/src/components/{ => form}/inputField.component.tsx (100%) rename client/src/components/{ => form}/textareaField.component.tsx (100%) create mode 100644 client/src/hooks/members/mutations/usePatchMember.ts create mode 100644 client/src/mappers/FieldValues.ts create mode 100644 client/src/types/member/patchMember.interface.ts create mode 100644 client/src/utils/chunkArray.ts delete mode 100644 client/src/utils/dynamicForm.tsx create mode 100644 client/src/utils/equalFields.ts diff --git a/client/package-lock.json b/client/package-lock.json index 0eacfc8..6bd0e1b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,8 @@ "name": "client", "version": "0.0.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", "@tailwindcss/vite": "^4.0.13", "@tanstack/react-query": "^5.74.8", "@telegram-apps/sdk-react": "^3.1.2", @@ -334,6 +336,59 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", diff --git a/client/package.json b/client/package.json index 435a96a..5148513 100644 --- a/client/package.json +++ b/client/package.json @@ -11,6 +11,8 @@ "preview": "vite preview" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", "@tailwindcss/vite": "^4.0.13", "@tanstack/react-query": "^5.74.8", "@telegram-apps/sdk-react": "^3.1.2", diff --git a/client/src/api/communities.api.ts b/client/src/api/communities.api.ts index cca5afc..81c9d69 100644 --- a/client/src/api/communities.api.ts +++ b/client/src/api/communities.api.ts @@ -1,6 +1,7 @@ import { Community } from "../types/community/community.interface"; import { Member } from "../types/member/member.interface"; import { MemberConfig } from "../types/member/memberConfig.interface"; +import { PatchMember } from "../types/member/patchMember.interface"; import { CreateCommunity } from "../types/postApiTypes/createCommunity.interface"; import { PatchCommunity } from "../types/postApiTypes/patchCommunity.interface"; import { api } from "./api"; @@ -103,6 +104,27 @@ export const leaveCommunity = async ( } }; +export const patchMember = async ( + initData: string, + communityId: string, + memberId: string, + data: PatchMember +): Promise => { + try { + await api.post( + `/communities/id/${communityId}/members/id/${memberId}`, + data, + { + headers: { "X-Api-Token": initData }, + } + ); + return null; + } catch (error) { + console.error("Ошибка при обновлении пользователя:", error); + return null; + } +}; + export const searchMembers = async ( initData: string, id: string, diff --git a/client/src/components/editCard.component.tsx b/client/src/components/editCard.component.tsx index c164ed5..7860f0a 100644 --- a/client/src/components/editCard.component.tsx +++ b/client/src/components/editCard.component.tsx @@ -1,20 +1,19 @@ import useCommunityPreview from "../hooks/communities/fetchHooks/useCommunityPreview"; -import DynamicForm from "../utils/dynamicForm"; export interface EditCardComponentProps { communityId: string; } const EditCardComponent = (props: EditCardComponentProps) => { - const { data, isPending } = useCommunityPreview(props.communityId ?? ""); - const fields = data?.config.fields; - const handleSubmit = async () => { - alert("АХАХХАХАХАХ А РУЧКИ ТО НЕТУ)))))"); - }; + const { isPending } = useCommunityPreview(props.communityId ?? ""); + // const fields = data?.config.fields; + // const handleSubmit = async () => { + // alert("АХАХХАХАХАХ А РУЧКИ ТО НЕТУ)))))"); + // }; if (isPending) return null; - return ; + return <>а тут должна быть форма; }; export default EditCardComponent; diff --git a/client/src/components/editProfileForm.tsx b/client/src/components/editProfileForm.tsx new file mode 100644 index 0000000..047f127 --- /dev/null +++ b/client/src/components/editProfileForm.tsx @@ -0,0 +1,96 @@ +import { useState } from "react"; +import { Field } from "../types/fields/field.interface"; +import { MemberConfig } from "../types/member/memberConfig.interface"; +import ButtonComponent from "./button.component"; +import EBBComponent from "./enableBackButtonComponent"; +import InputFieldComponent from "./form/inputField.component"; +import TextareaFieldComponent from "./form/textareaField.component"; +import fieldsAreEqual from "../utils/equalFields"; +import { fieldsToFieldValues } from "../mappers/FieldValues"; + +interface FormProps { + fields: Field[]; +} + +const EditProfileForm = ({ fields }: FormProps) => { + const [formChanged, setFormChanged] = useState(false); + // const patchMemberMutation = usePatchMember(); + // const navigate = useNavigate(); + const handleFieldChange = (index: number, value: string) => { + const updatedFields = [...stateFields]; + if ( + updatedFields[index].type === "textinput" && + updatedFields[index].textinput + ) { + updatedFields[index].textinput.default = value; + } else if ( + updatedFields[index].type === "textarea" && + updatedFields[index].textarea + ) { + updatedFields[index].textarea.default = value; + } + setStateFields(updatedFields); + setFormChanged(!fieldsAreEqual(updatedFields, fields)); + }; + + const handleFormSubmit = async () => { + try { + const submitData: MemberConfig = { + fields: fieldsToFieldValues(fields), + }; + console.log(submitData); + } catch (error) { + console.error("Form submission error:", error); + } + }; + + const [stateFields, setStateFields] = useState(fields); + return ( + +
    + { + e.preventDefault(); + }} + > + {stateFields.map((field, index) => { + if (field.type === "textarea") + return ( + + handleFieldChange(index, val) + } + /> + ); + else if (field.type === "textinput") + return ( + + handleFieldChange(index, val) + } + /> + ); + })} + +
    + handleFormSubmit()} + /> +
    + +
    +
    + ); +}; +export default EditProfileForm; diff --git a/client/src/components/inputField.component.tsx b/client/src/components/form/inputField.component.tsx similarity index 100% rename from client/src/components/inputField.component.tsx rename to client/src/components/form/inputField.component.tsx diff --git a/client/src/components/textareaField.component.tsx b/client/src/components/form/textareaField.component.tsx similarity index 100% rename from client/src/components/textareaField.component.tsx rename to client/src/components/form/textareaField.component.tsx diff --git a/client/src/hooks/members/mutations/usePatchMember.ts b/client/src/hooks/members/mutations/usePatchMember.ts new file mode 100644 index 0000000..123d4ad --- /dev/null +++ b/client/src/hooks/members/mutations/usePatchMember.ts @@ -0,0 +1,26 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import useInitDataStore from "../../../stores/InitData.store"; +import { patchMember } from "../../../api/communities.api"; +import { PatchMember } from "../../../types/member/patchMember.interface"; + +export interface PatchMemberArgs { + communityId: string; + memberId: string; + newData: PatchMember; +} + +const usePatchMember = () => { + const { initData } = useInitDataStore(); + + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ communityId, memberId, newData }: PatchMemberArgs) => + patchMember(initData, communityId, memberId, newData), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users/me", initData] }); + }, + }); +}; + +export default usePatchMember; diff --git a/client/src/hooks/utils/extractFields.ts b/client/src/hooks/utils/extractFields.ts index fef3f40..87bf84a 100644 --- a/client/src/hooks/utils/extractFields.ts +++ b/client/src/hooks/utils/extractFields.ts @@ -7,30 +7,34 @@ export interface ExtractedField { } export const useExtractFields = (fields?: Record) => { - const [textInputs, setTextInputs] = useState([]); - const [textAreas, setTextAreas] = useState([]); - + const [extractedFields, setExtractedFields] = useState([]); + const [textInputs, setInputFields] = useState([]); + const [textAreas, setTextareaFields] = useState([]); useEffect(() => { if (!fields) { - setTextInputs([]); - setTextAreas([]); + setExtractedFields([]); + setInputFields([]); + setTextareaFields([]); return; } + const extractFields: ExtractedField[] = []; const inputs: ExtractedField[] = []; - const areas: ExtractedField[] = []; - + const textareas: ExtractedField[] = []; for (const [name, field] of Object.entries(fields)) { if (field.type === "textinput" && field.textinput) { inputs.push({ name, value: field.textinput.value }); + extractFields.push({ name, value: field.textinput.value }); } else if (field.type === "textarea" && field.textarea) { - areas.push({ name, value: field.textarea.value }); + textareas.push({ name, value: field.textarea.value }); + extractFields.push({ name, value: field.textarea.value }); } } - setTextInputs(inputs); - setTextAreas(areas); + setInputFields(inputs); + setTextareaFields(textareas); + setExtractedFields(extractFields); }, [fields]); - return { textInputs, textAreas }; + return { textAreas, textInputs, extractedFields }; }; diff --git a/client/src/mappers/FieldValues.ts b/client/src/mappers/FieldValues.ts new file mode 100644 index 0000000..490ffc8 --- /dev/null +++ b/client/src/mappers/FieldValues.ts @@ -0,0 +1,54 @@ +import { Field } from "../types/fields/field.interface"; +import { FieldValue } from "../types/fields/fieldValue.interface"; + +export const fieldsToFieldValues = ( + fields: Field[] +): Record => { + const fieldValues: Record = {}; + + fields.forEach((field) => { + switch (field.type) { + case "textarea": { + fieldValues[field.title] = { + type: field.type, + textarea: { + value: field.textarea?.default || "", + }, + }; + break; + } + case "textinput": { + fieldValues[field.title] = { + type: field.type, + textinput: { + value: field.textinput?.default || "", + }, + }; + break; + } + } + }); + + return fieldValues; +}; + +export const fieldValuesToFields = ( + fieldValues: Record +): Field[] => { + const fields: Field[] = []; + + for (const fieldTitle in fieldValues) { + const fieldValue = fieldValues[fieldTitle]; + const field: Field = { + description: "", + title: fieldTitle, + type: fieldValue.type, + [fieldValue.type]: { + default: fieldValue[fieldValue.type]?.value || "", + }, + }; + fields.push(field); + } + + return fields; +}; diff --git a/client/src/pages/communityCurrentProfile.page.tsx b/client/src/pages/communityCurrentProfile.page.tsx index 29273f3..dfac042 100644 --- a/client/src/pages/communityCurrentProfile.page.tsx +++ b/client/src/pages/communityCurrentProfile.page.tsx @@ -7,6 +7,7 @@ import DevImage from "../assets/dev.png"; import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; import useGetMe from "../hooks/users/fetchHooks/useGetMe"; import { useExtractFields } from "../hooks/utils/extractFields"; +import chunkArray from "../utils/chunkArray"; const CommunityCurrentProfilePage = () => { const { communityId } = useParams(); @@ -16,8 +17,8 @@ const CommunityCurrentProfilePage = () => { (member) => member.community.id === communityId )?.config.fields; - const { textInputs, textAreas } = useExtractFields(fields); - + const { extractedFields } = useExtractFields(fields); + const chunkedFields = chunkArray(extractedFields, 3); const navigate = useNavigate(); const goToEditProfilePage = () => { navigate(`/profile/current/${communityId}/edit`); @@ -26,7 +27,6 @@ const CommunityCurrentProfilePage = () => { if (!data || isLoading) return null; const userData = data?.user; - console.log(userData.avatar); return (
    @@ -48,25 +48,28 @@ const CommunityCurrentProfilePage = () => {
    - - {textInputs.map((field, index) => ( - - ))} - -
    ПОДРОБНЕЕ
    - - {textAreas.map((field, index) => ( + {chunkedFields.map((chunk, index) => { + return ( + + {chunk.map((field, index) => ( + + ))} + + ); + })} + {/* + {extractedFields.map((field, index) => ( ))} - + */}
    diff --git a/client/src/pages/editProfileCommunity.page.tsx b/client/src/pages/editProfileCommunity.page.tsx index 057cb6c..14e94fb 100644 --- a/client/src/pages/editProfileCommunity.page.tsx +++ b/client/src/pages/editProfileCommunity.page.tsx @@ -1,28 +1,135 @@ -import { useNavigate, useParams } from "react-router-dom"; -import DynamicForm from "../utils/dynamicForm"; -import { useCommunityProfileEditData } from "../hooks/utils/communityProfileEditData"; +import useGetMe from "../hooks/users/fetchHooks/useGetMe"; +import { handleImageError } from "../utils/imageErrorHandler"; +import DevImage from "../assets/dev.png"; +import ButtonComponent from "../components/button.component"; +import InputFieldComponent from "../components/form/inputField.component"; +import TextareaFieldComponent from "../components/form/textareaField.component"; +import { useParams } from "react-router-dom"; +import { Member } from "../types/member/member.interface"; +import { useState } from "react"; +import { Field } from "../types/fields/field.interface"; +import { + fieldsToFieldValues, + fieldValuesToFields, +} from "../mappers/FieldValues"; +import fieldsAreEqual from "../utils/equalFields"; +import usePatchMember, { + PatchMemberArgs, +} from "../hooks/members/mutations/usePatchMember"; +import { MemberConfig } from "../types/member/memberConfig.interface"; const EditProfileCommunityPage = () => { - const navigate = useNavigate(); const { communityId } = useParams(); - // const { data } = useCommunityPreview(communityId ?? ""); + const { data: userData, isSuccess } = useGetMe(); - const { fields, isPending, avatar } = - useCommunityProfileEditData(communityId); + if (!isSuccess) return null; - const handleSubmit = async () => { - alert("АХХАХАХАХХАХА А РУЧКИ ТО НЕТУ))))"); - navigate("/communities"); + const memberUserData = userData?.members.find((memberData: Member) => { + return memberData.community.id == communityId; + }); + + const fieldValues = memberUserData?.config.fields; + const fields = fieldValuesToFields(fieldValues || {}); + + const [stateFields, setStateFields] = useState(fields); + + const [isChanged, setIsChanged] = useState(false); + + const handleFieldChange = (index: number, value: string) => { + const updatedFields = [...stateFields]; + if ( + updatedFields[index].type === "textinput" && + updatedFields[index].textinput + ) { + updatedFields[index].textinput.default = value; + } else if ( + updatedFields[index].type === "textarea" && + updatedFields[index].textarea + ) { + updatedFields[index].textarea.default = value; + } + setStateFields(updatedFields); + setIsChanged(!fieldsAreEqual(updatedFields, fields)); }; - if (isPending) return null; + const patchMemberMutation = usePatchMember(); + const handleSubmit = async () => { + const config: MemberConfig = { + fields: fieldsToFieldValues(stateFields), + }; + + const newMemberData: PatchMemberArgs = { + communityId: communityId ?? "", + memberId: userData?.user.id ?? "", + newData: { + config: config, + id: communityId ?? "", + isAdmin: false, + userId: userData?.user.id ?? "", + }, + }; + console.log(JSON.stringify(newMemberData.newData)); + await patchMemberMutation.mutateAsync(newMemberData); + }; return ( - +
    + +
    +

    + {userData?.user?.firstName} {userData?.user?.lastName} +

    +

    {`@${userData?.user?.telegramUsername}`}

    +
    + +
    +
    { + e.preventDefault(); + }} + > + {stateFields.map((field, index) => { + if (field.type === "textarea") + return ( + + handleFieldChange(index, val) + } + /> + ); + else if (field.type === "textinput") + return ( + + handleFieldChange(index, val) + } + /> + ); + })} + +
    + handleSubmit()} + /> +
    + +
    +
    ); }; diff --git a/client/src/pages/profile.page.tsx b/client/src/pages/profile.page.tsx index ad6c6a5..1c4f0f0 100644 --- a/client/src/pages/profile.page.tsx +++ b/client/src/pages/profile.page.tsx @@ -9,11 +9,13 @@ import ButtonComponent from "../components/button.component"; import TGWhite from "../assets/tg_white.svg"; import useMember from "../hooks/members/fetchHooks/useMember"; import { useExtractFields } from "../hooks/utils/extractFields"; +import chunkArray from "../utils/chunkArray"; const ProfilePage = () => { const { communityId, memberId } = useParams(); const { data, isLoading } = useMember(communityId ?? "", memberId ?? ""); - const { textInputs, textAreas } = useExtractFields(data?.config.fields); + const { extractedFields } = useExtractFields(data?.config.fields); + const chunkedFields = chunkArray(extractedFields, 3); if (isLoading || !data) return null; @@ -50,7 +52,20 @@ const ProfilePage = () => { />
    - + {chunkedFields.map((chunk, index) => { + return ( + + {chunk.map((field, index) => ( + + ))} + + ); + })} + {/* {textInputs.map((field, index) => ( { key={index} /> ))} - + */}
    diff --git a/client/src/pages/registration.page.tsx b/client/src/pages/registration.page.tsx index a339840..a031d57 100644 --- a/client/src/pages/registration.page.tsx +++ b/client/src/pages/registration.page.tsx @@ -1,35 +1,41 @@ -import { useNavigate, useParams } from "react-router-dom"; import useCommunityPreview from "../hooks/communities/fetchHooks/useCommunityPreview"; import useInitDataStore from "../stores/InitData.store"; -import { MemberConfig } from "../types/member/memberConfig.interface"; -import DynamicForm from "../utils/dynamicForm"; -import useJoinCommunity from "../hooks/communities/mutations/useJoinCommunity"; - +import useGetMe from "../hooks/users/fetchHooks/useGetMe"; +import { handleImageError } from "../utils/imageErrorHandler"; +import DevImage from "../assets/dev.png"; const RegistrationPage = () => { - const navigate = useNavigate(); - const { communityId } = useParams(); - const { initDataUser } = useInitDataStore(); const { initDataStartParam: chatId } = useInitDataStore(); - const { data, isPending } = useCommunityPreview(chatId ?? ""); - const fields = data?.config.fields; - const useJoinCommunityMutation = useJoinCommunity(); - const handleSubmit = async (data: MemberConfig) => { - await useJoinCommunityMutation.mutateAsync({ - communityId: communityId!, - memberConfig: data, - }); - navigate("/communities"); - }; + const { isPending } = useCommunityPreview(chatId ?? ""); + const { data: userData, isLoading } = useGetMe(); + // const useJoinCommunityMutation = useJoinCommunity(); + // const handleSubmit = async (data: MemberConfig) => { + // await useJoinCommunityMutation.mutateAsync({ + // communityId: communityId!, + // memberConfig: data, + // }); + // navigate("/communities"); + // }; - if (isPending) return null; + if (isPending || isLoading) return null; return ( - +
    + +
    +

    + {userData?.user?.firstName} {userData?.user?.lastName} +

    +

    {`@${userData?.user?.telegramUsername}`}

    +
    +
    ); + { + /* ; */ + } }; export default RegistrationPage; diff --git a/client/src/types/member/patchMember.interface.ts b/client/src/types/member/patchMember.interface.ts new file mode 100644 index 0000000..fafed99 --- /dev/null +++ b/client/src/types/member/patchMember.interface.ts @@ -0,0 +1,8 @@ +import { MemberConfig } from "./memberConfig.interface"; + +export interface PatchMember { + config: MemberConfig; + id: string; + isAdmin: boolean; + userId: string; +} diff --git a/client/src/utils/chunkArray.ts b/client/src/utils/chunkArray.ts new file mode 100644 index 0000000..f6d53ce --- /dev/null +++ b/client/src/utils/chunkArray.ts @@ -0,0 +1,7 @@ +export default function chunkArray(array: T[], size: number): T[][] { + const result: T[][] = []; + for (let i = 0; i < array.length; i += size) { + result.push(array.slice(i, i + size)); + } + return result; +} diff --git a/client/src/utils/dynamicForm.tsx b/client/src/utils/dynamicForm.tsx deleted file mode 100644 index 4b337d2..0000000 --- a/client/src/utils/dynamicForm.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useForm, Controller } from "react-hook-form"; -import { Field } from "../types/fields/field.interface"; -import { FieldValue } from "../types/fields/fieldValue.interface"; -import ButtonComponent from "../components/button.component"; -import EBBComponent from "../components/enableBackButtonComponent"; -import InputFieldComponent from "../components/inputField.component"; -import TextareaFieldComponent from "../components/textareaField.component"; -import { MemberConfig } from "../types/member/memberConfig.interface"; - -type FormData = Record; - -interface DynamicFormProps { - fields: Field[]; - onSubmit: (data: MemberConfig) => Promise; - avatar?: string; -} - -const DynamicForm = ({ fields, onSubmit }: DynamicFormProps) => { - const { - control, - handleSubmit, - formState: { isValid }, - } = useForm({ - mode: "onChange", - defaultValues: fields.reduce( - (acc, field) => ({ - ...acc, - [field.title]: - field.type === "textarea" - ? field.textarea?.default || "" - : field.textinput?.default || "", - }), - {} as FormData - ), - }); - - const handleFormSubmit = async (data: FormData) => { - try { - const submitData: MemberConfig = { - fields: fields.reduce>((acc, field) => { - acc[field.title] = { - type: field.type, - [field.type]: { - value: data[field.title], - }, - }; - return acc; - }, {}), - }; - - await onSubmit(submitData); - } catch (error) { - console.error("Form submission error:", error); - } - }; - - return ( - -
    -
    - {fields.map((field) => ( - - field.type === "textarea" ? ( - - ) : ( - - ) - } - /> - ))} - -
    - {}} - /> -
    - -
    -
    - ); -}; - -export default DynamicForm; diff --git a/client/src/utils/equalFields.ts b/client/src/utils/equalFields.ts new file mode 100644 index 0000000..3094c2d --- /dev/null +++ b/client/src/utils/equalFields.ts @@ -0,0 +1,14 @@ +import { Field } from "../types/fields/field.interface"; + +const fieldsAreEqual = (a: Field[], b: Field[]) => { + return a.every((field, index) => { + if (field.type === "textinput") { + return field.textinput?.default === b[index].textinput?.default; + } + if (field.type === "textarea") { + return field.textarea?.default === b[index].textarea?.default; + } + return true; + }); +}; +export default fieldsAreEqual; From f5703481b91f8cb02fc13fb875422271989761df Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 15 May 2025 01:47:49 +0300 Subject: [PATCH 30/78] registration in community --- .../src/pages/editProfileCommunity.page.tsx | 2 +- client/src/pages/profile.page.tsx | 1 + client/src/pages/registration.page.tsx | 116 ++++++++++++++++-- client/src/utils/fieldsNotEmpty.ts | 14 +++ 4 files changed, 119 insertions(+), 14 deletions(-) create mode 100644 client/src/utils/fieldsNotEmpty.ts diff --git a/client/src/pages/editProfileCommunity.page.tsx b/client/src/pages/editProfileCommunity.page.tsx index 14e94fb..9296929 100644 --- a/client/src/pages/editProfileCommunity.page.tsx +++ b/client/src/pages/editProfileCommunity.page.tsx @@ -68,7 +68,7 @@ const EditProfileCommunityPage = () => { userId: userData?.user.id ?? "", }, }; - console.log(JSON.stringify(newMemberData.newData)); + console.log(JSON.stringify(newMemberData)); await patchMemberMutation.mutateAsync(newMemberData); }; diff --git a/client/src/pages/profile.page.tsx b/client/src/pages/profile.page.tsx index 1c4f0f0..b0970cd 100644 --- a/client/src/pages/profile.page.tsx +++ b/client/src/pages/profile.page.tsx @@ -10,6 +10,7 @@ import TGWhite from "../assets/tg_white.svg"; import useMember from "../hooks/members/fetchHooks/useMember"; import { useExtractFields } from "../hooks/utils/extractFields"; import chunkArray from "../utils/chunkArray"; + const ProfilePage = () => { const { communityId, memberId } = useParams(); const { data, isLoading } = useMember(communityId ?? "", memberId ?? ""); diff --git a/client/src/pages/registration.page.tsx b/client/src/pages/registration.page.tsx index a031d57..dd993f4 100644 --- a/client/src/pages/registration.page.tsx +++ b/client/src/pages/registration.page.tsx @@ -3,21 +3,71 @@ import useInitDataStore from "../stores/InitData.store"; import useGetMe from "../hooks/users/fetchHooks/useGetMe"; import { handleImageError } from "../utils/imageErrorHandler"; import DevImage from "../assets/dev.png"; +import { useState } from "react"; +import usePatchMember, { + PatchMemberArgs, +} from "../hooks/members/mutations/usePatchMember"; +import { + fieldValuesToFields, + fieldsToFieldValues, +} from "../mappers/FieldValues"; +import { Field } from "../types/fields/field.interface"; +import { MemberConfig } from "../types/member/memberConfig.interface"; +import fieldsAreEqual from "../utils/equalFields"; +import fieldsNotEmpty from "../utils/fieldsNotEmpty"; +import ButtonComponent from "../components/button.component"; +import InputFieldComponent from "../components/form/inputField.component"; +import TextareaFieldComponent from "../components/form/textareaField.component"; +import useJoinCommunity from "../hooks/communities/mutations/useJoinCommunity"; +import { useNavigate } from "react-router-dom"; + const RegistrationPage = () => { - const { initDataStartParam: chatId } = useInitDataStore(); - const { isPending } = useCommunityPreview(chatId ?? ""); + const { initDataStartParam: communityId } = useInitDataStore(); + const { data: communityData, isPending } = useCommunityPreview( + communityId ?? "" + ); const { data: userData, isLoading } = useGetMe(); - // const useJoinCommunityMutation = useJoinCommunity(); - // const handleSubmit = async (data: MemberConfig) => { - // await useJoinCommunityMutation.mutateAsync({ - // communityId: communityId!, - // memberConfig: data, - // }); - // navigate("/communities"); - // }; if (isPending || isLoading) return null; + const fields = communityData?.config.fields; + + const [stateFields, setStateFields] = useState(fields ?? []); + + const [isChanged, setIsChanged] = useState(false); + + const handleFieldChange = (index: number, value: string) => { + const updatedFields = [...stateFields]; + if ( + updatedFields[index].type === "textinput" && + updatedFields[index].textinput + ) { + updatedFields[index].textinput.default = value; + } else if ( + updatedFields[index].type === "textarea" && + updatedFields[index].textarea + ) { + updatedFields[index].textarea.default = value; + } + setStateFields(updatedFields); + setIsChanged(fieldsNotEmpty(fields ?? [])); + }; + + const joinCommunityMutation = useJoinCommunity(); + const navigate = useNavigate(); + const handleSubmit = async () => { + const config: MemberConfig = { + fields: fieldsToFieldValues(stateFields), + }; + + await joinCommunityMutation.mutateAsync({ + communityId: communityId ?? "", + memberConfig: config, + }); + navigate(`/communities`, { replace: true }); + navigate(`/community/${communityId}`); + }; + return (
    {

    {`@${userData?.user?.telegramUsername}`}

    +
    +
    { + e.preventDefault(); + }} + > + {stateFields.map((field, index) => { + if (field.type === "textarea") + return ( + + handleFieldChange(index, val) + } + /> + ); + else if (field.type === "textinput") + return ( + + handleFieldChange(index, val) + } + /> + ); + })} + +
    + handleSubmit()} + /> +
    + +
    ); - { - /* ; */ - } }; export default RegistrationPage; diff --git a/client/src/utils/fieldsNotEmpty.ts b/client/src/utils/fieldsNotEmpty.ts new file mode 100644 index 0000000..961593e --- /dev/null +++ b/client/src/utils/fieldsNotEmpty.ts @@ -0,0 +1,14 @@ +import { Field } from "../types/fields/field.interface"; + +const fieldsNotEmpty = (a: Field[]) => { + return a.every((field, index) => { + if (field.type === "textinput") { + return field.textinput?.default.trim().length! > 0; + } + if (field.type === "textarea") { + return field.textarea?.default.trim().length! > 0; + } + return true; + }); +}; +export default fieldsNotEmpty; From 1d8a686257075d450bc4333041ccc5737d4bea71 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 15 May 2025 02:40:30 +0300 Subject: [PATCH 31/78] create community choose screen --- client/src/App.tsx | 9 +++++ client/src/api/communities.api.ts | 2 +- client/src/assets/about.png | Bin 5250 -> 0 bytes .../src/assets/telegram_avatars_cluster_1.png | Bin 0 -> 41663 bytes .../src/assets/telegram_avatars_cluster_2.png | Bin 0 -> 51677 bytes client/src/components/createLayout.tsx | 11 ++++++ client/src/pages/communities.page.tsx | 5 ++- .../createCommunityInitial.tsx | 32 ++++++++++++++++++ .../src/pages/editProfileCommunity.page.tsx | 5 ++- 9 files changed, 59 insertions(+), 5 deletions(-) delete mode 100644 client/src/assets/about.png create mode 100644 client/src/assets/telegram_avatars_cluster_1.png create mode 100644 client/src/assets/telegram_avatars_cluster_2.png create mode 100644 client/src/components/createLayout.tsx create mode 100644 client/src/pages/create_community/createCommunityInitial.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 9df50eb..b5f703f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -16,6 +16,8 @@ import GeneralCurrentProfilePage from "./pages/generalCurrentProfile.page"; import RegistrationPage from "./pages/registration.page"; import InvitationPage from "./pages/invitation.page"; import EditProfileCommunityPage from "./pages/editProfileCommunity.page"; +import CreateCommunityLayout from "./components/createLayout"; +import CreateCommunityInitial from "./pages/create_community/createCommunityInitial"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -54,6 +56,13 @@ function App() { }> } /> } /> + } + > + } /> + + => { try { - await api.post( + await api.patch( `/communities/id/${communityId}/members/id/${memberId}`, data, { diff --git a/client/src/assets/about.png b/client/src/assets/about.png deleted file mode 100644 index 7b0699a396ef5cdec2eaa08ea4f49990d7bd534d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5250 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 diff --git a/client/src/assets/telegram_avatars_cluster_1.png b/client/src/assets/telegram_avatars_cluster_1.png new file mode 100644 index 0000000000000000000000000000000000000000..376fb12f5c5a0b4a02333eb3a6426d877a53a5c2 GIT binary patch literal 41663 zcmV*SKwZCyP)@~0drDELIAGL9O(c600d`2O+f$vv5yP^GuxHC+`_G23$;eUY{=0eCY z|G8YaGYJ=FSi<6jkOV?vd%R@vChwbU$=0q`@3mK*bME`T-}jw5)#{e|l&ZU{TJUDj&-=Uy~AW(aqcVcYK|1qpSVqzI0pv1s?9G zzMQ7}P4gxF4PU;&m*H^u5FOLeX!NN5eGl`{J0G2eHlL4+;i$gsov#jtg&vk2{ADT@ ziwEg)j6Uqw(oF~S^-KAJUCQg34uwL8=<+BXPxF^Ckw_f6T)v3C^U+!8wDWN>T<+&< zHPIuiHiN%><#PFeZQBRvLwy(he1L8qLwA68kNib^nqK<@YvD`SJ0G2eEYCZ z#^~asyhFU)c8M1Nr$6}1ci&2X`FiJLLG;c?cVX4eSGS@^SQmhQr`Y=vUKIT0`^<9g z9DC;@5PIjMyRhu%Yb~HhSPOu5i@Zzx_1>4THBj2)KHfQ|)9L%r2K3IyrJ;8|x(n?- zUuy|H!qU*i;g8$4{fS=Ay=^#3JI3Fq_x1}xca6RCu`KAFkM2Ut=WDIvvL`IwEm9Gb zbMNo(eG!)xpQF^#r!Mxz>z$7kL+^Za7nXFs);ca*!s?|pRtKz-qT^HZCBD7$u{!9T zkM6=^=j(F8Wk*=OFJfoFeDU~t?|iI2dgr6NaJdrKWkXnurpgVQAkhi7Y;tGRML zPnY>R-Cnfm($)8K<-Q9c6AjU!PUUDa>|OG0qsb69r09CY!j4=-?%TuSu<0$X*68z6 z;p;A{b#wwdlG9{q>OB_`aN*pZy|SmHiMTRJzHVKXM8;%OsPh3Lr;r>Kr*#rqE-+y!;sSCpo33{ZkzE!w zt~EZyskT&{HF)NXJ}aYtW72<$qF#F)&w2^=ZS+)dy}i3y6yX|fXPxX7v9;FMw}st7C>bG=mK3V~hZKb6U3K8H1iHT9{< z=kx3U{ppoT|8dmxl@nzgnyArH#ahOJp%{@@40jTNbt-vr)HFFuwZ0PMSj*t_A(K~M zrgO=Qqp0zcilF$ooY5C^Fqp83z*dyJj?#K~_gad!)&ya#WcrUIr_UcPT^1y@7&u*c z&*nr&kQYZzO$c3;Yg~!V&p}n^+_VFPhkz9>j_IY zHhvb%I8%p7>MO^~l26HR-)n<}9 zTc(s>Iou^bW-5p8ufVNtE>)5{rEv8s=W>Z^2gdrfM&!8NY~Ls5Ke zXL?zqE>`m$KH+wKO=`Kpkya)}=Vfqtqb^qVJvSL$^EIjE2G_ipytZWQWsSPJJ=*HZ zPZ>+qDXRKAx_SW10FI(Q`)aWlO|^(SM&tN_v7|&_%K@%c9X{@Mywa~zz_qS*{gy;u z%K=vSJxiq3?MlB+!BE1%hLkM?woK^SXshc!Vauri>=9La!BN+}yVA>&F60R7^rTDm zEA+5LaHKUlxIF2?I-QR<^AW0Fb(fm7lfhBf)=Yi5(uLFPl#xoMeji-Q^_%BKbf2D)PF*A<^CodUcc zZ5CGah-ziAT*TE&5w?cqBCcMFaCxwd#MLz^!jAt$u?o<(?Z0bh3b031s|&7e{nGwy zTg4enRwu@laBmI%*GG!zVfDds_;#xbPfw~8VGqGEm}|ePkn38<@;%a84zL`)-KxS) zR}pqBVYOSb{&^y)&keB%{7{J?!Ub^KKnh^#7=GUD+B(%Av*yy^gMy8XS_ylp3}(AqK!X7`Nk z8`>9bO_W18JR9kWF_sBzw!3?veQ81}!Q5A=R6f>eVvLUZ^t8LbsuxvtA2{0LG+{fy zO~yw~1s7x3b{#K1`%OIXkAH<@M_)j-T0tZnl4vR%R)kuuR^|3gb_fUG@d3Q$uJ@sD zU<7Lo9Bpx$upQtg;}@rvF2*qTvgk_4t)nnEi^;PmF++!A*I@BnEV?Y^XXs}-$@Fs~ z+lOd8Dd&nJ1>}Y|VtC^g+F;~RBeHZ|x!alrM_ZgGY!eRAd-#D)5@U1>VYO2U*o&&V z8=NNm%q{(GiMB4LAmoK_>f9+*Yc=n-Z$0?;xc_T^g>tbViyJR|kw`?yDon)1mnIch zE|*X&6tQk}D?avFJISgSRjm#1fXRP*b+#?Ft@)(oIu4G!{3IU#&iy$2(vz5; zI*VGZO6k5T>UCSL*QjnKpv5m_@q}O^Dk3euF1OLomi|3`c2TWR6c$Th^Oh@c&2@KT z{Ooa@IevtQu!L;3A7fYU!xcNP!LF-sLjTZubQ^e}5Ajy%1(;hc);ZLY^`0jt&SnBRVS==kyTFt zxHj>@?MZx$(uA4FrJ1T4(mL|mOSu25e~y=)e+>0nS%`-hHLgvSN@W)NpjNNZ?^MLa zkyI*`L^7E|JQfqu;zg3z$y%*0slgf@Wqrx%M1D*vY9UJZO(YUj^NLG^#<%m1|JJ*{ z7dPMjZmi!phHk^Uew8A;)QQMUR_sAor47Q=$)kAnsfTd*$wx48{B=}{GqkBt%YRrl z2bR3&=8~5!wQo?HhGwErB&7~TV7x)8({)E4(^Z$@D6*8oz4|Q&ar^gt81cbP;?@DE zMUkVtdbNc7#3_pK&SCuM%b2C>{Fvbg5g&anqI7Q8ZQhCg^_$Q)G>UL4Lw^^BgU(4k zZcK5C&}qj+mOmc%m8}Yp#`YG(%b3J15Wit6&CbTO>$fWgWfAKeXz)BDJU zDdHhS`90?JtEEz~9N}6_SWpqKkV@!$)-+_ru<6_|ImtPq)M4XiI{4i2{j3OvD4qL` zdq08g2kw;H3KVf4e)3^F^XS)b`p65gc{4&A8jBvooO^jC^tz%HRfnT-N+L$6-b<$cj!fF>;@fiC5@bp|S1-Qn+DZ*d6HYXF1 zF9l~#pT;l#!q4IKsgpu9d`Og3Q&Cl=S``B0+DJGOMJyU6vWn`um0BQ8RK`1gM@Itk zI7D{EQb|aUi7iGE8UHPP!R0!WRwSYxizBg8se(QGZp9DX`|}ta-hgfbM_Al!+;@rk znDH~G@T;HrNlctQBLpC+S0;k6g-BRMrTnwmC|HK9EbF{Cf{Il6u$R$Ty zbdKvbUFQ@qU5``R_NI5f4>c=>rylqRjGs6Pr&f`3%+DXuf}3zS^M+m$S zGGXxo#%Vw%DivWV@-pPdP)frp_Uyk6KlW3 zig49PYsCqxU1as!&&=Yh$I9s8@&nhke(LH=Tij2dI)#7plRu7Q$Bs$*ugW`aCM&96 z*}8d4?e0UKL>j&Ylfes=f0u6x{AO|GD8MXAT+fOKSt-RKfAyFwncVn!-+s@B@FPF| z?`3iBHgH{Q)9|G&B^+V>=im5c%uY>s(X2nJVpu-0!@_X10Kz(txP(y5B@-6C_ceb1 zt7?N$s#LY`riNmrMD@s;5Eh>+K6mk`ine1>-gq#HF|nE`j|kCqBr3O^kPsHv%^dkz zUFTG1gvcv9ycyTr{x)2F+q*EZVLPP^LvU7@xwx*iJ=dM2wW5T@O~F*uX0mFz&x%uj zT(9EcialJuXh);*%P+r#fBDgmU}k38OAk7ZEsLI)_K{kON>wcgO-u~4I7-UOSHt2< z-q9Be1zF&jxcG574av0>PDzb!+KylUkAIF#ZUEhgb~GBFeDf&&=y!et)pDsRnlhx& zNTykszjS%>cKv(5SkRv)RMA#lDLW+^u1S#*mB+i~N&z7GRichh}CD;ja7 zBQACiTykCO5WQ!2uVi!aiV{|#P~fKE_oGGh#1%dG(GD@jW8e8UKKi3SqNL9{Vv>W2 zYLPlgXx?IKYFZKknDi6ZPi*x$Wx|RRaYZ9xQ5H(Yl__3Jm{H-7K$x|XmQ?GR%e zdHF?r_S3($z;6^`DdL$=SPe33{=K?>eza8=VyR0?ur9Snj&FJDcVwm#kce3!?IkF*h?fO#AHikRkJO1ooi)|+7P*QRxC1^*&@bBWcsoH zuJ_`mcYP4K4Pz^sDr|=sgFgQcq*AH7u|ilegD)bhU#E+oK#S;+RS)x#ue*5oOci&o zk0TRpEtR%u(`MwdSv>OaLtY9fvKwA`jtFV_6@)7FSC&36`FlXb z9cJgxQ5F-|)qAhU@cJzx*X~AeCUqtc^krI?y$=kpLn4{R;TN8n_Zvk(&C!-W>Y9&* zh|T|4>MlYuvdMs?2szrdbuE$w8DZTtZVpN9(#C`9Y$C|vbI++`>D3EMs!$wJx2T!qQ~*NwkO_S@p0O+Cf^^9=Hzq{2ZQt_Sp;GuSpT6FCDLIR7F@) z538vfNu8?rmfqp>LTifumX63w1jf7YVzGc{o_vT~QL$}oH`O~XO%1tPXa{L+xnc*( z#UhR$d2QZrbjo#+$k+e7#_z4B-F`vG5w)2SQ3zcR$!(Fw z2FE81F;TZ^Z3t=cj=7Ilqx0%>w4tHT-__p~W8Z07(0E}Hl7U|6&`<_0v za&1aA5QiIeEq|)fUk{_-bMIE2Qd68Z zRKlF|9XGrP+NV;qZFRlIa(YoF@+GAx>r9v`Jse9Uam}6Y#$6x$F+}@E>Hf=~CTvG~ zP>V1gz%pW43F}f7fjzS7AqXy&wC3h!@!=o*ew;davZ;>chaoJb{8GQG>ok(9Yf_GF z`|sTp4TQ9$ol{E6GDzRkM{Ioi4R1M!pZJ;IMU>J*i$xQij!PvijS(A`uqXv5G}aSmm-OoMFOZ;z~%Hf+DJ$1tId!Hkb-c#~xc*~Wz^}Bxs1R zg&tY;u!6W$(qg&$zy9%$ab`e%_V1L9D~3c+I`!x6pfyP~)wyuK8-tgw(kaA+7Fk}f zxE^-Z-Wze(+dqK)HylJN(}!-vrIHqt)qnlXUlAkXrC@2K1to;wnpI7y1TREb+((y8 zDA7fd$SZLX!aD!?eA0SNmUS=UvL&+e>Sn&QBnvgz1!e!+-iN!t|Hoiu)-8K&s|}=e zl-|$lmPy zhHk^9*0d6-w48_YMHs=*9HqJJyaWA%-8pZ| zkQO_=w{SYN2&07KM-E@~zJB6ZOy$Y&r|C>YV~EP6=L})7$ikeTm^oJ=|EL-3ya;Rl z^O>Jb;MF7m@&9bDoAjb;cSdn~3P)dh8k@(iMBnfzoMo)$7}8>=_fTt3vuq-aw(Dng z-RhHQ5x?@pOpmO3xMc8W=;PmPJuC6UKk^Y#Yq5He9o*;gdC!#s+IERaMVd;bKPQ27 zCN0kHY?zy&>)Bim*=!DJirAzM!o_<&#Qi-XJw1G|$6!p?M)+s`eJY(oitfwLQz+!| z_n-g2F?H@Fx)tJr+;O_7KKJ*W*Lr^S6T;6Ts{|8?!v2njf4a%3+^RAy?r}97c z{79>D>3WCS@3@}iJ>qlLF!Fg(ClWdGt33DSEBK55^z(S;Ykz^bwY+Pjano-V^nUz0 za{=0nwntdM{L8=m6rG$=w17W&Ax}Hw3VP_F9hj^*LR@!_2A7C4fn2k1AO7r5|6i$b zh^PW;;bD>r>Dq?4vD0DP*kdtun`fA-cjFQLv+*ZX*HzDs--5{O)Ojvf-KSzZar_8g zdEs#)u{4H9Hg#oskp4+RTz%=_+jf5c#;)3f$N$&Yn{K~Q6t&Rp{u&cIQE)9wMOZ4L z;?Xi3VW~)~=0#X$fTtoAA`+IVha1xMg`&EJNNeF5n=eh+_+u^jvAn)Ku9a3w1sr+) zDO3uxxMJ`1a6`){edh_yOk6D*<|gRhdhoY?>$m=Iv=MD3EZuL_l9L4++5XShdb6y0 zXfMRVi@x{Q4FzA|hlYkJYRcfj2kvj`t%+(=+k_^nsCGVRs#EC^6sG@%7abQ3ufdQ{ z-fqZB*$XLC+>&;Em(-)o-~y&5&*IP%590O1&r%eYLI1!ydfx8p*DC4#XeJq)w6eMW z^O}kmB8iKU7DFjIU9Yj5W#;t-Qi7p*jl-rFD=Gs${SNL6*RSSdzW=0tWli=`a=)h6 z?Q>Eg)N2)-cxi^rjK1M(&I9<-2XAxPwU*3nm9YLdDy7TsKkxRkonkx16oP2MK5s@Lr%!s1BS z)EABLA|!4Xp{9`|OMM;Z*U}urMP5qM)xXpJ9=-@n-DcZ0oO$yw)pQ(e-+P0MHCZmq zR72uDn2NMSTDSb_um0+TL|RAD2DI&8sr#)0gGClR&#H$WmJ`4I^sIc=TETDqhyREH zs%P;+m8AL!Q>cOSp zLPCg!Nr?-;VTpeni66#|ztL0G-10F0@t2AV5v%1lVfG7kc&1vtiihs|Gd%GRe~Kt- z%i2_Xcs7c%-P$wId-d6=si}Un0d0w}T1^GA^ZHLdIxV$^9(q_#ROutus&bHp6RBhh zPd)X7*Oy~z8frlaD?-u}aYf-}cEuszb)yIayl9F0xsb?|`m`_}dL5{X3 zVK;RvcAijEr)?csg}rp3r{)YdM_4ku%U6qBh$#K(!1-bg^PZ?5U7xr^-)7B=zO)f= z-7e$ki_aiS@5!cJ`{?hMhqn0jT!M12ZBqA3j6BMVKrL}|Zb0dupnn5AQSW`@2? zMIoP%o|zSySIjp%?06zBN<@yf_%;z|(BI9@&fv3u_$fU6jh3CzRumjzot|thBE0qO z_h59(Hn~1O%2_g8-`_f_0o*o?;7u+h{=;4LFt5dr#iG0UBx_QG@HC6Q$pL)b)xBDt ziEc>A?J1hCl?(XlpZ)=!{rcw-yR>m37X%A1j^$gU4EpzdBA?H++ zzxsv$j%uZZZov37AFVC2@BV?0oOfF*DN%QkG~YtXLU)nBU+B_cp$IVO>-3^^Uyl~U zqzWrUT3`O`XE1m2)mD3_mV}eV2u>ASi{9yVej1mEOCl^{%zM#-^%B<-{>uw_^w7g< z;BypZwNlU8f9(Oh{{!Ez({)A-*iZ$kY+&d-TONlB5gD3)-%1^8AuJX(ii?Vgw5&#bt6Z4DU;O^35Uxxv zqXdKMkNHST^(>aT)8_0GxCFHP@8VP-otR%o3t%S;u3z=g!)hR3bMf2H%%T-=LvbdP zX|h)0ov$j;8)>>)_-v_-RK+!eNosm}nm2huRL0$#6=>psq3a?spCg8-5@j6w`Cn6i zkzdk8Ollm3>3LlD{q-N=#it)>DdJdZuoDK0Fj~Rg-~IiFQ+;thE|wzn3n+|fv5BC( zsQ7{=NN&^Bg`TTj#I?}l8Y?Fwr#ItDjvam$U;1yKMm)THrItFqHHT+f>;K}0;!6== zv`koZLYb__5D?sN)vH_eu-ag$dn>6y?#uen5C4d#AT#w3ksoUZceC(u0e^-_iqn4_ zQPt};iIRAcVWNt~6XKE~g8(^_5~2!gE1-r{dOlpYxf#_Lwi;x$_B9Fh6o~%V5^880z@E7wx1JW_& z+IaebFX5#J|Bgrt%LbOZx0o92kH_OJ1v-qD2ulks?nOXw8s>8?7QK7uq1|XTHTb^w zeE{p%uh+!nDg~Fhs3O7G5P`kPqQ7yO;v2W@ z)pYt#rT*9*BPqfP>8axA$`e$mucE1exh}j>6O~Gp(ug(f6kkQDsD=yX3q=uZP8Zs&J<2?1m!}$E4{x02CL#N|XQiI=j@4xo0FQne{W4^dTqAb@rPkgZu zO#03L^9B2;=KcOc&#}}LrvGskdM@q*oIHCHU;Cf`6{+y@m0m6}HTYSyfWU;MrHS_< zAXpVBHaR`?&_QT5HMnp8eq3|SKDF>Laj|OxkrkWea;=IFZUmO7O1ojijf;CsIk+=K z;!Z@9&lfN=Gpql029)Q=Ed9&}KPD3(*TOip$VR+;6zE{0Dp$&qZe(&Zk(}}?jL7Tz zr+@f!{I9S5rM%8gN2{s9?YpkV*seVnxQ7Kbi;9I3kQr1}nf7aIizY{>2bEou_gB_@ z_jg~a&uTW#WdyAmE*AYY{ht441$Z9ImDly;!(YRZrypu*tjki-YHE-L7__lFh=6G2 zpAuM_*h>ZW(8=J>X1*SLC&~g0laAvNBuprZMCRl+hRoC=auVltA}e0#SaiYDjg_>0 zoGvFkQN}b8)%5g~^i<7Igf>Ty#gEI86d!69S5*$%<_(~$Q;iXMF5Q;gw8&P8(7ybI zKf%%0o<}!;Q-gL3fdSXDOijvv7{5RjvDy-)s;IT9d;|6G{P$-xYDDmU_6{v_FvJxU zjXK%lzxPBSxOmiP;6-zE!D;EQ{nh`2s8e267Yt4f)}5B#gTR@(K!ioRyMy$G97I6$ zgcdz?GB`EZN<%Tr-1qFcT2g=<>2UN@C>Dgo7@ma_9W0q<;+dSBr08jq>RWTD=s8=Q zHZ+HsI?FB#(#R@{sfn5_SXw$S5mY29B*hN#{9I}=g*+$b_35yU(-;5hzl!v^)4^ob zN<;A#S6z*5J9m4jR5MMONxnoWR3?1cROk(WF&O4KSUla=PjU>ubA=~>Y4Cx-KX`Gm z^x#4%!6v~6+$K_*kA+f)3%xsjf;FFoBhNpMr@!$xh%Tc5gZFx^G!(ZIWd(YcR%GlP zU450(fj#ul$>4_KLlad5#gE?mFQtYg>AJk?t>X1X`ldV=c2`G81^RtO$VdceT-%DM zX|^(|BGMQ;648=&u6wC@S!#TVI`mi*o}m>}O~y)qQTNPv!}__sT|B*}Lr*+{LyvtU zaN24$zzxOu7J8rG_B|iO9M$$_ImJ4g#}uVhr>18yNzv*gU7wuglqyBkR0k|^U64sl z(t?hN9aym9kFe$wk^kpLFVw!mex_$^+e3GNGXX(y{S7x_@4kIvt5m1- z5-&ii$+xkP6?8g}f6o$lAt+V}N=+-KNlDeWB6>=(8C#;7h~wS}6En$Jl!i=5v}m#K z9lN~qdS`5-OkWNk#>w_&<60Jm8JN8_I^;@sR#Iz!(DJHGCJf`Q0 zm?=;sT&$o_uA)?_qfCTVGZC$=JOyR$nd@S{C04^GsR82&>HqzO(A0u3FUs<#2Nfwb zh-p49RP$QkNilxn2%df9tIJx;VjRmae=Tdt|AbbeD;8R`vTN<3ht&WJFk1P%{+oaM zaXn5QTJfi3>r5ub*p`)qAw5G$ML`jruCsij*0qVMo}cAkFnLkp1y#~pluG2s)T9eMTl0_f zxKBU&4Oq41wd>*tYq_JWB|W-!wK#}?=m{-)=r*(xW!-Y?ZFJl&L=@5LKZw|&qR5IC zB~}H7ylKM?cW>iG81fbHISMn2nG$rAJkbS5a)zW>bise3hDjzp7kO?%hK>e8;pSa0 zOYtB0=fCX=#UP`VDC?R7*JJm-{i2>U2iFTL6k)RGUWMLIzRu}cPT?x?MWc_+Pgdu- zubUoTXJKjjh3Z)IZ}&)x$;x)+P{Gyt_gfqndw-PW@+*fP!|7KJb$yhzq?g}H-xWt! zJ)uPp-G)}9tRMQ&hb0xrD;lSjn5dYrcu@<5{f-r8foaI3`Tl0%6d?vJsAYX=7F9=! z0)R#e$_$P)sYCyCTrWDa{L^gJBK!1{4`Xin9J&FmMpmBd>E}5If{ZJyTAhL?k z0<v_6Pb**Ba(yIkEII~8zt+>lW0{K0RG}M0!Oorz49g8KnkE`yt zcJke4u~eR%7fu<|^SE?QnD1=cmBVqgvxNmT^n*C>aZTFLh%@|TX3l%l-_dQ~e(;}B zoH)^n3O^89iLwaqUy8@zMcb5#@!xL+{1^73s~)-;CdxW6wDcM7xm*rE@-IGuPyfz; z#6Tj7jf4Hzy>$cw$q=RMV#s7tp1Ln8#d`BaJ0n9LDF(*ma?@!_x~lZvmq9T+bA9-Q zpZgiyf8U?Td!0+h)k01GnJtt=#-1-$F;`UW1c_)8yLVrKojb?y+zYSbYmYud@0(d3 z7bZ-9kC)?wy;`H|x)&5+sL0oxD+o#XqcqOqTK>V*?!t4o7E2W_c6%{DiXsjBGT74e5wnGM*Uk zzP5$~Ls5L@mVrQj!#nw3{=s)*?~aW~C1V&K8pO!D5yav#J*~DuI3e9DWBW{W>$aXE zOt%$_B^*6FgRedE1Ri?vC~CB*DGx#=mycAYR9Hvi)^)2z~_GDhxJblA%lutlpmnEJshKGcVrWinSQDzRdC|u zB)<5KhcPi-LW0g^EE<=nHp-$IEtrYuL7VYd6p46D_uZ+@M`(el%n*r4+1Y7>W%U(K z`PQ{diR(pl^5=T@1JHA434}&a_8Xmmip%KC~-`gBv;B)f}0eD%A1!Z{+dE z&v%EMn$b#>m8B@_=YR2M;p8U~kE;1qY}cd~BDH`S&pHOD>OxXn5~P%i3_@twV|pm4rBf+_6tqQ|MeOe%%j_FajO+;bbQA6rj3`g&6{srNS) zJcoA?5zx+ErK(&ts>Gk7qU(I07oL6;-Go+ZSvP#whcEbhe((5wX0l48vPe=LGL`M4 zpK}-(8o_(telu>|xmg_V>or#MHIF4RQh1{>G<{tQ8SE~U+B1UHYA#& z9q1n(9F>EkDwF=Z5SX;O2^?hwHEfRdk5c_1Cg~=Lesm*mSl6kf|H?NdmSO1uj11}Z zkp|#~wGuC=S8W@`N8ft~ZrnZ!mliXAyyk_B@2&d2SovlaRDT*V>{Gn);`oKn|1l=c z97nf+Bdk)uT;YY-zMV$p#svUGmWnb^bfo=1}jB>Vc&*FT615mknwf5M-_=5- zwUFS7Mno55EhHR`0uCBjl(YIY4y)yxiMZ2 zV{3*sPoY2r`93;+f1zJ2c-nRE)d2)RPs+ZwFhA|r4zlV-u#@{&js=UdBK;!>Q7Xxw z5>ysHVZDfj8)`7i*f52>+jy}FSfz6x{-La-WzvRw&Bi+bR``usGV{8D$)WxeJUu5z|{&Qj2PanNJkrx?*fwcUJ?I9o_<|#?AZfw=^;=}bhKN{XBnvy^M|6K&*i%>Dx%e(HL@19|66J>wYe17_`VT#MILw9ER?#Ge!6o#-eVDSYHcnp#jq<`04 z3*CO+DQ_hm_)t$7*p)CMivRJ>jVqG&du6(NH#MJsJy^R&B)1NsSlsKs5?BqJA3q7f zrxa@aWDS%f{;XF@k9d26xA;?eaQ6n zBbDt(f_@HDWX3hgE7uR;=DizH%JUFNmHuNwV;6W9VX!kirw3~_mCCEv9p%bCUs$1q z5)%LVMK_BGEBw=u9be;lFjt7^x-nEw9d5qS+xj+os5ta7&8WP)w)C#si;$!S23ZUh zX<4%Hg=vw!_Nr}o;kDy<=4>8TEauI_GU*_$p-E%1em-+)%NPw^N2A+bdhscYpFM%~ z8^_QMXvGjW(KiaKJO#hO&%$e+dXL!BDVa>d&St0{H7Mg;sufNRR#C}Q+~U-*e;aK~ z=)63B_zdh+7O{wI38a4IdX35sqHeUw{bIhVZ~jcI`S+WD8-Ra0JDX)ED2QntZb>tk~=3 z2#eE#{8I2CDXCi3@`9n`3x=TTp@wzBxlt!p(cfkaTM2 zEqG1M!dhwO^1{fq5V%bWw#jp6@zB@)N`BuBV8W^fTz1@;*>JeNW5IiZNr%Z_6ou)O zCDWo7yXk)qmGlv1#;qH@oxuUwC9BNom7_5%@ zl6S`ZhH`mT{wT|Jyn`Rj@kdx#gy^76saR23S=k_i(}HC?aJ%yR=HGDMiWPM3RWBXb z+2|y-UpLmT{0Q2U6;AcR3P-)4n_a&f3scCKHuUeBjfIaDGNULO3y=(c)J?!!H?BiA z8G~J`Ha(8zZ%Ea*JN_cQuG#S8un^?2Z$E%iVFulTplU%k9M_G*OT0gC9Eh~y@dVP8 z3heLK4aWTgL|QqjgC*$PQ4NgV$lLc_i7kV16bpGpSoKEZZ_PJzt6`SwEb4}_*lStn zHuHGR1m&Lhcg>=V=Ct8T;cO{zTJXU9v|v+8aBgnyAOhmyGd;0IC&GC9t#sP&Lg?lQ zixD&}$i9E!bU*5qMnkde4BCp<{#>|x)+m_h&IG2qv1)H&17-Ij*a-I z=Z-f$k6fC^)X|k%8VFMhj`d{zM|ddWr|k4NWT<{euHr zG4<4eoduKj`^NjXu1ebPLO50I?$ohS3kE@WV1swvzfjHJ>6?Bmxt}+dJ>*3|=I_NN zgI(XDT?`x*1?=BFhN1pUQ%cXD9%9%&Q(Uo%@--FnbI;C9k=770bfj`Q#8`yEHdzuWyLkJ}yO9V{DzID?u?3SF*Rzam zlUO@>{a4M=m6?UL=xg_mW+LLhv+M4HZW#W^t5MGaRwKkRYQZ4j7AybH^H;NL5dcq3 z_M9y`0o~H%+l@U~^YPH>U>meZrXO(U)g(B&((V=_lJG2>Lf!}v0}~+ujeab%=okkC z{y9f=mTdzmZ)&c;VbrM7OnpVAx+2YWGrbhP^86FxH`gtg$p>6`BkH@c(1PxVx5#Us zb3RYpr0hJr97xCU*6Xi8tvsjtwkV}ruPMJl7GYG_dypp;+scbj zgcmNn&q#B<32Jy+sJY)ttiE_+v9F(tQ^(45SM~MzQ-@6l-5w#5T@B1sFHPR=bR9@1mVmeYBOhLQDVXAO4~TxhyG+7ESvC*Q+Z*mD~IvsskV&?xkggY z3*G6{Cony65~)ldx&brv)?p;jpGY0ctt(PKPl<=~CFBMsHONx;S~ZK>z#uBsiX7}U zSP|=rn)Fj`-H^o{`?lkOLvJFHA(E8o#mZ;I(i;WWYx-4nOM6~Acfr^06{rBSi81F@ zJ}KRhO!^Xc67~XUxRs)k2-joI74SyZWBP{yx9z*~fo~ie|16L46M8W-28&loo zc+Ti{!G!^P3v%stG!8A z#hpnC4do!OakXk0ufO^nHg4O4ZUEP?Sl%8B@Rbm?APTZ_J6e3;cUkoAw}=RX8?(8g zxK`^&wNjDk&g_OE?{&qN^TcQLs_ny=E|hTS^>M@#DIMXM$k`RkCQ)i4)Ew3Mo?o}( znH$b~jb7T9pVMkYRP);;FfV$Grk3CMb|G*HnC0z>@X}MctQN$4!lHlpwQhhy+^fCb zs}%u9QFm=j;k&nII!Zth6eqh}aN);80lft$?HK;_zOUfi%p7JbbxH~8?-{2C?J6%< zv@4eGDp-_`J2sieOUKXPTQ42O_K`vC8QX;2V;hl-aox$0U37>RK8NkzxrlA4d{UI6 zrftJnJa?SPFdX&jCG|Q>t7{_GklrHp%;V?^jRn%|(wW$fzj+wl1g>MP>km*Hi6(Vw z%=N-lOC@NV?xxaeu5!$pmoDUbR*FcdmdjBJwMgl|k~oDF*zC4i5?6kTt#R${Z5W>^ z;P_MtiKxnmSe9-8k7`q7x8YXd5tp@4iqP_i$H3G+ZC`V^AuN9rx%zjw>Z3xpC6mFt zG24mBN-%FhP76Xe5J!AB21*I`>Q~E)ZcO`K2x~!bVGuMGo*EdzD<{Wg0dmxSTGB>r zbQ+FER3oow+12e4OVy=#uF@+f&f@6VNgO_Y8rNTSCAM$qQ;VJ6HCz4)I{mPH(+CD% zI*wyv7o<{wyz@8B$-K};!;R>W(@?IL5>yKy`wF^Fo%pmnBKcC#;60pZO0Btz#8QpG z#`VG+^LPASvUO9co|cAUj<71#s;Dq4JPV6ziF_^-^j5Rn{VR{Yh>3iaYG@HHQCCs1 zM7n6UW0GSY|9Y}^7Yq6a%5C%JZOK11yl^I9bi)R$77De%iVshCA)iQH)EYwo1nv6o z;o|7T-l=w>+a>LO1fBafGP(ijbOwd|95S&mMh9{j>dRqxpdXn;6e+4*v4s*73`ar} zvjs{Up2eHvQz%lg-l5x`J9-kYA3KfxySCx(8?Gi&;~lhGw3v`gYG|-Oi#=P{0oW{(}l&MjP*%87FVM?~?61ta|9HHB4xOfuE_4xE`O;rZ9zz_YI%!&IS+ zB9Y6tpLrQ6ihgdt`5O5fB_OnPYKT1sfjw7l##f$v1tn=xa+}C1qRGn9k(GLk#@|an z*7O9r30%Vp>fUZeQ*cVN@Q?lQerNek$U?Ox*VmsFOCXuHk*QUr{>Mj^4x3WFb+JEk zrJl7eo5b6$9mAKt`HE6!5@Xe%`su8w62PFzNvk2N7a@gLzdAn_Y@EMPleFFMPiUfY z{y9MrVKg(iYJ^px8dgyEb^yFUL%+%l9Hfi;gs|uz6Ns=5&-VJSE)`5nOi)ZztC9Bm zQ0i;fNq#g=^kcinLdre9A{9U~-PR2Eo-%+N+%EVs0D zIT4Q|9wS1`q_J)N0B*Zx2cA803eUXoGREhMc=6OZ+-&olC8}ewA+C86S3TysTpW8g z595iWXFZ~dY4>(n`8fUODqA1*tjpsWSW*<%juy&V z$J81cy;UJfDY5(ay8bk7*}WB4Z5qTIrzcUVR74=aE*C}(W*g<8IAery0~Pz9dgH9P zS;*vJ-+km+w6aiHMEU!>Adg$GmJmy>j*_eulxX$OB$r+rv7*T)!jjt-AT5S}gvWe? zVZM!{)?_k+T2_q@k;%Z^f5n@E!knZDB|S*bcI{PLar)dGj*QPqYS6AJrC@?5QHPby zk|GRW9pnrb^L{l9-3Zr_Q0p!(pF0N)eSP z)fjQARPUl9J}aa}@=RY?x-AloW5<>aviM2gj_Y$|*C{891WQXdGUNGpJ{J5Ah7Tvl4y!5FGdQ@YQ6JGgf|5#O7q%wO=e@U;jN?H z*=t4G6o`K#q@_itw^L>abmK`U>jX;q`&iE@yBA^JqAG@Q%vdh0 zB}uzMP{N9)`jmv37D}7aQRnhSoSw@g5>Mgu=`+Ze$|$=`W^r0rY@A}jMY&A7_mDUx z@CufQMnnp4*UE^~#ylO1pf43elCBR^jf9K$5v3HM$4%nS{X6m3k3M&vQ#-GmGPTJU zIry@LNF;bj;p#Xcw2D$d(XthZc~R#4OF#S~2LEmIFZu68>Yh%CwN*{bacd&7aBUW0 z6txJ0HZOD<58k{NfA`OiD{^DfsK2XqS)M&rf}xXvzRH@Fj1&tYIY6@TI32MiLNPfa*WN6Z-)%C zI99xxh6p6sF<_#sM9kfUcZB6HixTkxDB(b zOjs2s@VeN~Tt&6M=va`yVufIS4S2#W2Rf;Diu;~^?$NUsO~?J(&BH>l%Zas^5|r;j zK&9ZzXXfz6o8v@K74enh9dO9VnR%CuMwFxKC9W1nI8A9QqXsmKD*vq3>oC10x~MVX zRb|17CDY(oDSB=k^-^ikg;wl=^lfa6d$AN*r-4<1L6w3cZ(qddw^{hGu$k<90yA#< zGNo7#aIEXb?Rr(64+_2XUqrBdeFisNwFOVSbWGpI0-UfG$mp49z85H1p{`yVBIJj|nJKood>Fq)|O|^*~xc~zJBXNvR&3KDjrCOKK_nhac+ciC2qT%Z& zGmEr{!bsPpuZB{GAyI{e)KEuH0&VIs@Nz{w^QuNge_lk{yU_IeplR`Q^>9eDNIUa> z-yPs6D;Xq{8RoQLqls(2=wkkW#fwEb>OLGHQJOZHC8BbrCyNJb+H?;1eWf6a$fDv2 zD{j4dE6z>iapWw0E>M=u3eP%2M3L5oNYekh{jXilF>PYF78$nL9fgR~sbSaN>+oYg z|Nq0e4q3u00ST*CtBpm1*01Jzd?n~a%Ooa-pMT^mo*2Klg*B5E3oVu#YsukaK3FNZ zBe(QaD);OZiY0G=qFid12~Oi=QwbuNnA{d8f^oF*txGkmxk3S@az%cQ#kkQp0{$P9 zmQsijfpL9H9uJ;8x-JofC9VvNg`xsiGn}iX?!)UgbX^UL!AikoJb+Sg-YA!g$=HKH zL>QDBbn97Z&gSzVQ;4aKRb|r!TeTx_-SJ(w?!aGtZQnw4e zw)w&luc4Sps~(nA^BwQ~0p#7ps@Ja!3dAMpARdquTrptNLwF z7FPT`r2v-_&8!qzG1j8s!XRjBJeSL0Zgx`UC-rAD*f_ckTi4|<+~0>>UmpfiF|Rc= zu2YR}C<&$~F@_c`9<|Pedftd`N=C6|YzTWcza8H=^fI1$=`~rTc`=M}V<`LJ zvMkzBl@Bqv)kGw{SeEL=vaE$g1B{{JQS7j;<)Y7TshSuc zL+eH{ux=x&9hB)U*8>l8+!mq#^aVK)K}UplGdPN3qFU}cPpi0?Tzx)PnyeUWQE*`p zG{V|E+=pwZ&|aH4gLC!9d@6IvX+6_d#k*pTkht+T9*c;pMU{%^{4@93yz9UY^yjkp z=WjhF1ZuVOCHK5Zk43^dYT#Su^%BvIxm-W)e&+|JKGuz}1G-6t zB1#>*7-=mQ$t-2P^gmuW#I-f5O{UX4!IL%yRq4?ZH+^pCt%kLKzl~e=ZNtg&DV&+g zqgF1c&jAw35oM7`YyNBU%kic4+;Y*kVJvV&v)r%)rUW?~7`TSDR?#g^=}VQX`>|SN z#aPP`R?ze~k0meU^Ari#LJ&1yFN!7eVng0_j;IP;nk$@J5aA^_+A5VO1sFpj zUd7EjH{sOT37nmtL$z9OS}0BZDr`B}F_OkeXdND;+E>lgtMnolp^FVeIoxvK8idB5 z#iX6bz_x3L(84?954w?0?UBO1e%)&t+9Y7X}t`?54=G|8-FD-ptTqbDakxXTjZIY`R!}$o?7D|-45)pn(fm zrAMDfSvsPN^!Ew+{KOVB*ySuxhK;Gz|9?c&tANxX4(7H5ie6iQ`pmX&z>X>o)!eX=Z?qN#Q1 zC~}!3hSD+Qcy1QmH=W6#MD?l!5g<1pZy8#LTsn!o_9rs(bbj3JLs{(Il0g(TL~<$I zvS$lUJp4Q=mhuqXLS*%xTleFtt)mzo>PLo1QdYRxLzp=-jQ%Tc#hQhC;mE3y5@a={ zQB;P|7;|sn$kFszOjf!+=tr2+bKDb0!GivsBj?nap=)z<%5yMeWAny-yyuQ<@P&t- zq0b22$F0Dn*C4L$fCwr4uYaNRr4SaIEk~2sutQc!K9moh;!-^$Sc;w}i$d{|K;IUb1F4rebrCeO^ z>l>6>)6hVl3|D;p=o@(T>=a_>W+~cAV#{zA>$3@p&?;h18>3oQHe1FZ?V>jiX7S>& z0xfo7FS=s4_pP)iO@v)(V&&)DwPhGrY#hR=@oB8fCGlNvxe8Zp9>ic@hN8BpkQMie zv5ipam48IEe+$w>o6!vjx*%IVN00dp!tX2QK!Xg!{9o+d}3TOMVwlaD2=YC;}NX_RD{Kf!|%HO3e3)x=yzdk zSx;m&Fn|;-pj;~}QIs|@F^NJ!8S_rO@(@zDd}v8RgVjc0!eVlFIOS$}5_UD(#SvAX zA{^~7VwpM-yN>85vh)B=@aDkp`KaLP+vM)1fTSNc>auxp5}{1Lkd-Jm>GjM}1XY-w z5KAXEf!Mw-hYx<&4fxxCeiDV6EuWEC#F5&jh%j8A+7q_-@8Ilch@O+v#~=B3zkq5e z4s=*Grv<_J+D{2`ZR$VWKGIbq=Yz`C4LXUe7;Dk=_EPa(Z@U{OPMpBRsbk0{MBSFQPP=$Jf7c3a`F?6!*O2ZoKdP--mm?>mEFJ=m~u7froJ9_1C1%HB2eO zB-`IoO0ifdQN&dgS$m~s#)QxU%j!ajB2?NDddT1Up+0O_*Doryk#!>!+4UnvL{_O( zF+P5lBDsejp`_q#zkva%1B+e9$0FYmCPDbepmXj zRF#RE3dsB1g4SLy;ob>6tl{=z67GLWNSp8U8FJBZ#H@B+8zfnIh7v5^2$9 zglmj%yAEIf){8hZT|k9yV?`*ROG}TRQP1vZJ1NsHVd0(+{wS`x<-3qyBVWZeO<25e z^8(&22-*bfN2ie$V=W6VTE*Fu$1r>5B!;P;6pn;w5vyam=!*0>8znNK)LfO_)!#gf zKl#fE{L6R}*WGjrZn^92bomy1`5*oP4}Se0ktA}8g;m-wr5OM0hjuJZ8GE z;wj6!O{J7nUr#QZLw|oCGMSVR-?@niAuA@XY&IkPL80mlN^|FsU896$rt^vbLM)Vc z2$}6gEe!MialLRlU&7?n3=z%@Z4@e~5$RO9zlzQSt3T^CTfgTaO08B=CPLz2juncg ztJSKc4h^Z9$WG=|Id%EFbRvq4BZC+n7=&Azf?eZoau*v0(nMMZ@YGAk@z{%RqUP45 zH9|(#K${oCxcfVzK)LR=ci;zp@>fu(c7AlcRIF*jV$ngjpw*~~(TQZmSnD;cRxtkN z5gfedJ=lKLUVQZ{U&eFKKBwvvtRkZoE0fIQ>P-XKGSrXBLJ{Bj)}z?F|2iboX=(g@ z@B2Q0cr=2?ANpqurTayaE~pg56ql7~Fyk@ASyx*$S&kCT^QjrL~VWpha>F2tw(`t%bDnvBjdi*Io`||53 zmTIPjjW!ErcxEFtXs}=k)tY~`q2@D3X|{$lle6fj&BygujUt?gz$VgjEgQ*%gzlW3!_WWXFH+5DLVo6WbaJkMr;gTeaPM}Eq#`(e_$3^E@mcJ< z@fJj=9+9QvJsq@~7Rev+akN()xYRCb9VN0HK*nG$#Fc%$zb{s_x2%dXee=7^&U zehm|oQ|KSaNlh!0${>-5i@0NU_7vS~Kh`274anrgjl4B=*tNQroL4>Rd4DR29izjT zn4ZHlog?=9D{EqtIue=*%Kx4(cKbrtnQ}s53zPXg9)I~5Zac6`ydrC*0()>N8z(Ar z{NO#;;hC3Dq9!3rkYk9iDXjlJL6OrZBFfWI38iyB36;KU|mI8JlUC}F0Mn345ToW-J;Ta zTzevt;PW&`#KHuYFVp?1HAQfatpyzN48~JLD8KN>FIIJ)M6V&N+%6bYEPw%TrXY7B*lw#AzzT+xSHUF8G;Z?2fC0ojfk2f`6oMZ^+{yjTdq2NP1!^8G9*jq%r7f0t6Tuq{7EXi{(!kBiQ6B3OWg}xc_bkNVnpU}7 zK`b6ak)mtN&BCgdq!mIrzgt3J+~2ireGczAxL^9bD3OcWu_uvPzY}?M=6Q>>aK3^Q z7NsD%CCuc8+aLMbN&IhD_v8AJL`N*{tu_Js(dlHxxExY~nSkH8iL<9MJw1cp`Q6{+ z>8Ut#_N+uOB1I1A$I>H0L|Im?OK;R1El!v>8Miz)gKU^mRhg_TXmws7DJqgk%27oN zWdY?jJPTo-LQA!IpkS5$mQrB6h$bm=;~t-AR9QlC^D>iXL{g6Y{A-J_>XqWczfioT zW|0}WyG{+7_tr#AP~4+k@s+|*NGTkW=3XHsEnDaJmZu3P=zWe+iv&Ixi%GnOYeUw9GA2p6B;W-3v*$Lwh9sna`QPqe~Q-Scip}Ru}BPYI+BSL%Hzk7 zDNewOk9M5P1%pX8AdaqiS-T+!2B!zP7i+c1>r$gCMmLZZV=bHBwhE~+5Xo^GX?Av2 zxm!TbEW-JoaNHNgNa}tiw2_x+pfEL#>Z!wsx;6CWGRPN;(u-9IMbYH7;5v-~ z>%MxHnWW3)#?b>GOp-BLD0!zZDa5d{WMV=EkHm1+=J`}9I*K;w->lMc%I)4b-kaY` zv#zVgw>a8$J!FW?fsT-3aH-~Z^4tuLpPfdL&P$q7djq)y`YCda5Q+3BQ%Gd`ktD)m zKgclMo{C54bCD1~LA&mvN~uL2ZSU&nkF9Yn+Kjkne2k@^aHyPqiW@Craq66xcm?&A>!hOGYNry?JboYD{FqBSY@mq>klI8MY&;Vsr~xF)oJ) zYcv(iXOEk4W3>1UXOmP^FRMk6f8p!ykqB}ix$O@8SbbRf_``MGN-yI^_9@kmElV!OTNEM zy>%_2LLDp3YFstc;1NDeN8_z%4iK0Dp3L8g-Z=vx#$O5y=dp zBSR|9i*7bU(&;K-Cs~=%OgJU*z%HBJEAH^H<8s^H@$5Yd5Wxf72GhG zMZT)t6g2#{JzTB7{>n`l>hGi4Nd;TBZAIL_hWbW#^XX%W=4R)L|Ln;{+~p=u??5S+Z?*bXHA0Mr;tv`hBQGpDJeY@ zS;w3DYlZoDR3WNWBq}>Mc@E*e&A9iY|4y7LT&gL(_~;8=L)h}+K3e9@TD*Py$CP_`t4(H$fplaV(Z8-2KuNDRhh#--1oP*^Uk+o z^VJ71v|%$|fA&#WrCH6B#!T1M*B#Z+OpnVC%~nu#I_FGYX-Nf!=^Vx>(yr0ZStPRk zi01|nN@UTghzE?Y=>3=pydq~J-LYu43V+(LRq|@Zuy!$Y8(A?fmmaK4z;nVRQjbuf z-6>(``hJn8pPMVvF1bo+H%HQdd$*3_&iy;FY0Fkg4RO)lq=FchwqpwG8k0+feKx^$ zZI{wRGctFnE<14x!^x=BN9>xdBL?+43|Vh)363NqZY|@Y~ye^VC+NZW{L+m9u4wGygR&H z;3qF84mtYNC8y{NSYW`5^viVo!_j=((qWvc{I#2hyNawBmrD;;&S;0<0kZ{q7r49Wh$g2CWaaE7}5P$<}#igz>BZEO82vL zB;^p9hOu}1Mnw3rEG3_2yB@O`J38WG;YZXc19gPQPac+@E){*aR(E{r>-I;QTwTgC!8F>o?E{Mr~c%BU_+Llkm^NK6@2lFUzB4(=)C|F z;P~_`_FplAWYUH`QxWmaKrSowG|P%$^WK~B>;q?zvZ_LmEVOV#{t3crASC~Pwp@`- zP>Q1C2&D&O(F8^0{mAqUArhm1$H0h0;hl;^Bv^z+n};{JhIKSZ4J*@Si)XvQgtU^8 z14nkjdgP%0_cAP7ehi4v;W5-oY) zW>dh63Y1`s(o6JqA31hHe&)aDMK?p~v5iB0lsc+6>L9M$oZPdN(>M2Ph4jLvES*e0 zm?btvHrwoW!Svx+z;cBp^T(pflP?_B(Uqy&F?3|bLo8{}P`8q}^ZW0`$?-fwc9q_< zm=K!rBfKDVl+5;9F?!BqJce-8lIJ#QVUf6p6WF}}7QFf7{phEYXmOtv*Cc%pW5_F? zFN)|Uo2Hatn1vXuKujY}2j4&1KZ;5RXLIABU@uN;Sj1Y>CcUr}?95)4GapwJJY4c; zuN_3U;&KsO1Xauy=VnkZ<#GGY4Y-D)t1K1S!={m!zk`x`3W?K!keAJLWQ+JgoB&;w z3W8UM;<`l)hh8`=JzF)saPr@78Ok7nX7g5~fOGXi%&rdNPLAeHXrq>;Q-_IusgZO# z0;dG)6;C}GGIgtvKD6k`7mZ&J%Oqkw$-6(wiBJ7b3 zT^Y<}q)#fwF8y48OeQ_$rYJk@3e}$m(Kk4R6h$CxL~Prt@l{C8X+&DZQV|JC`SL6^ zcGFL!=}4s!B{HnniFi}P=;W_r(CQMMlS6bqLPvxAQBWyw55dswWW~5#YFIn^f^FWa zCtrbc;zd~~`{EQWId-!WOfQl`DvlaoZ&AvMYe<4nNT;k+S};mQ_Oo+kJonl$Ne$Vi zskNHNRU3ztY}-->xbvyiw)Geips8-77Hypxlsj+_P`WE@nUVqfjX1^X2`qH|O1Ynh zWpqf0-s6bW?ko&$-;b?3_Yx6QFfcfPbUI69z!6#qo7X4RMq?4U`n<+ygLB2^b%;bm z60r^sZ;+8N4Z^aC2&pECIuTnANg^S2Zg|aA=TU?*!doupN;Tx@9~~yrV;ikxivA{^ zqR1sC5mzLY?P$47I$%nW0;}mztyVi4U?CpxE_j*LC%WRwGr#_o2UwCQ_(U&Ety?J;gkRO0}*OZ4@nit)-CI-1y#}gJuS3x5zi)vVgq(Uar z;s#(QDoWdNU5_o1La`LCxZzGzxMhIej(9SPYu|D^N)%N&6vFJInp7XFEEkdgTdpc4 zz2)jHxMI_KBCa@!sCUkfGh zsQ6vtck zZ=lx@YKoFo^KnRsl$|KP_iZ;|fKq}ST@?_;J>UHvBBiRNWm)#_R7-Hjk03`R$;P_I zN#4=+TBd%*uf0we(}fZ?(re?A}hb?D?^aGe}Z_T!apmYWifjI=YgDD5RQLIaOG^Gvb@o_ z`thUDFr^uzBAJ)_GhtP0h~+we2x=?!D>S)SXtLr);%Gz+aFt@xr6`6Xm+}OPCoCj~ zH=~La_TGFwuDR)U9DDvd$fSU)$2MSSupdvpa2&_a%%Ds>9}T-0?a$y{ckIJ$*Y86n z9!0)f!r_y7-1Y7cP((C{GDT6+K2SU-4klsC7H>#fyIhE%!%Kjl>K~EB8Xt1dg7a#-DugpD^6tk3yNFtD-o$ z$0_O>>kCnYPCIr=IfX-E-ABb~F2~M>IQvPyF();s5x>pTXq0 zH<6$e-sa&f#@@c4HWZGGr|--3VRU2!n??sQn8~0@8;g_Uv)H`%M!e_!-v_5sM65#5 zap@ewlxuW|;A-@Ki!GB~Zw!~zH7-bBm^S03X!`c+u0lGYB5vMjNNWO}Lw3Iir~2V^ z@ZiQ=ELenP+OG`rFnfJ;sein)M_9{(2Tl~bKFT_EnWO}Reg|HC?R5+gNo3MF<;tyZ z7Y74r(owcM7VnUCPY@3k3`e695lzk(@W?kG!^>};LVq@c7Y@HJlW?om3X)+52R0-y z7%qc&ks>Wi%d{eIAWXH)ur!!Q<&sm7b~H`omyvo~ry{7}f`cl)lczkhs?;PPGtE2wMvk+b_RJms+98MO@xZvLTa1{ zMbs9hfcIYaR%EH(6pqv-0;@S;J<+)CO*S@ZK-&Q1q&Mb)AcVyVsnKK@r#t;!=`wKt5?ea`r`t!+jbO669D1-g zB_32E_`(a%$!x1fpLrS29XWvkDtx4A2Oc4!NXDaH9g9&8No4 zF+MeoT3x-rJVbDi7U`RIj>+Zh$|CrDQ2Zt~?v`#$!6qK@0ezY7h!j<5No57i&w8fGkDw%w2h>8zK&E7Hu)i{(p z2|uh6>~MR767Fy`UO{YdD`q-)X=cGiP=#Q*Ty78+t1nnGNLR;}`Tz%pqWJ3Z9$~c` z>_f;yCz-Um!I|BUlb1qjISQmQY%^{{G1%uSz@+nE3}=?oodB$5eHjHx<^ZXgaR zUqWTcWP6i|9iLi|Z>v`8^xQ>E7Hw?YvLBs};Fe5PsVO$qhqSkdWht{+5uJFwX(6#q zV&hvwR(#1L={<3ar|N9@5GwE+u1$1ZN7ta>k(E5ZAuRtP=f@WDhaoDp(a=#B4}>I= ziZEGmltl;EPIHA)jvoq>lXjuQb*Oi#-W8PX4>`fWTzTXD2-JtuWmtJUal z`l(K{XWIts+A=CC#Y$lsb8~aD@bU1)EEV>%nXF7RWOsFA=sA=R=;wqWl`A~!tzpYKKL8DEuX)j{Jp6IUZly% z{N8H})|&%AQL5)xuc5A^E&uCj{H-B8J7?{#vHL|g;PUCg3g~cg?ik*> zV;v?bg?@6XfT9zUiT9?zh8H5)(bt`ZdwRW!6r~Uc(s2xB;uKA7z`FjN3~j8H^O&8U zMIk>ckrIzj@5|+o%jS?F(n?Zv#S5CPt(sIcHC*;0vbqwVW=ax_tx{u|IuvM-Bh|3j@uD{hefhxkpx=FZQi$MZ4s^a<)8$}#%MR^93G2)&hcFbYW6MAe z*Nv^iOf`f#iek#N_!LT2R4HW?7pDduQy)f}NM|4srUHEiBmG%gFlh%~p<;e<60^l3 z=kw*Tq<4zQiioN&*GGhvMKYBT_w~B1XI?p~mJ;$Uq%x3DFQDwM;3U^!)*41(){(SQ z|JYk`^=&^yi*y$l-bPv=tkSF|DK*X4AJzD$537LJ@`N$-XN8?Vo(0}G5PuXlKe94W zl{2qSWj9IKb+fRe?&n?FDwWG^3~a~5lcM=Ez^|-abrJ2`DArA#3Z8)#v}u^m#{=fN z_lcyGz#$GXgW!(!F{)!-`hdb7E{s2abyjLyot`4R98!Wo)qxDBIE@6m7tBpdM3c>B zuxTVK4WE%{LKI#+*GhKh>=S5l6jkKEL}_jURf?#JRLd$;I+5ki3|62esRqU*#gP?T z^CY>!l@~9L2JCrhCtmE(NKx^@wW>&Z2)F#`?;_d10o{mX3nm;ky`RnG(MZJ^!eSwX z8{+p*)t{7~pTdGE!Fg-&e8O7v@I_s4o5@ZIKiuIT`Pf5 zqMygDbu2Q$qbpBv!SKF)T-3_v^AFI@;UJcX14BK+S}AmTijbqh%RwO+w0<>v@+kV# zDcTVZARL(^;wfQvHZLl`WU3%F2u|s7|5ZrbEz|;8tyN{wVv?$AqH?r2gL`~vr83+`4+Yud|s4GKRg_smsN6kSz2JB1sxens#514eMcn7>`*;?_Yd5qGYS<3FH>m4ZnTzC2UK zkMHg8sA%hQv1DR(Y!En<#v2?OR7R>qPUTb?`9hvD8a8HUW<9UJkWZH_&7bspj-t2_ z&ti;tA}$LfM^&k0N{A|x$%+~<5sQgQt65aJCgrIC4N1wOs2<-K-y)L0rv2~eiaOO| zU|Xaha4Lm{6_$m0q|itWO2e>cXJt&0&KF*oI+Z_NXfB&;R?ShF>ns#;p?O2$OA|Vd z?$@I0{5fK^X*{kz6a4!krHJEWW0>jSbRZ+B!v+1@p7#ITwBpelbsxR)AIB2G{aE}3 zJ~Yw68p<_@FCERxBxD}&c*DBRXzHA+ul?wXGYVY04T9RaW9aMa69)xW?s0058+*A1 zmCa_fT1^iDgfds&h!e13gXenn*>X_g*f6Ea&a}_e8r%< zh)mBPLCseWy8b$s+nA9hp9$W0$o#SzB5^wRRNv&!1Ak_!)e`bf0)u^rTOB34R#RkTBAIGkj~!tnV4vK*X?7W! zNy&@ae82NYTmIWj1mzcFC=y$!cBLDP^{`ET9zOrcR9X%8Bm$c$l#$+XBW9~sN6)_+ zOa$N5(&p{~|IbZ?rR!LSg4D6zy(NLqy;?vItAjs!VH$t`Mt;==8JC0Lf?GL)pgMWd zyB-Ngkxr|T6)ZciR%=psFdlNo=+un-2>AvoGGR%d4PQnhqU7U3bs_O9kh2t-KyK++jiAB{2h(y8` z&pEDTF=?ru99tGlhFR#miMY^~LD~aQUHYEP-yH0^O>$|~blSYA_HDbitHltzWx&c*UcwwH{&JvpIOJus(BE1~t0cal$Z+asZF0vG(-4NVtlc9bx^h zHwVe=s(Ar34j3%lkl9vYOGi}Ny&J}j0bW|GQmHn~R5=A$t5QU#qKB$&VcXt=T`jg4 z%px#Ya1qhbs=mb|FC%zRBRm{&B;gH(Z6_~CN(N?gaWQq^!bfu&(1>D;$Dlfo{22;K z-Enqm3iWCc`C16u@A!Vy`?k^fMMq;K6+9hCpZf>q-*(=r){4CS2ezkrX~7Nw6Bf(g zxzYH$$1+$6^wNRnqn@9HTPdnUop%0q^r9w98GNgRm{Kd)H|=VN`8bF893aBUpdK zyOBS45|!C;Y+k4Gz#%-ku*1#$JEp`CMT!ll6$d? z;{or!IXbg+gf_npBK=po9^*KT9eS0v;T&;g*I$WPvV{<(L(qykur@Q^NUwV0iG?B} zCXlL(cd1Z)h~H~>o}j|qe<4y@D5~=JUNu`lp;5b%x}za0C1H2<&m0-osNU9h#SNG! zStzGA!5uw-X=@0jE+DH`)PZz<{$!!s7bGm&b$kv1amV@=TF!e|RhS-vR8)F~9qrPYu*1EMNaQ-K@B_Hv~x5tXIA?Ib10Kchi}On9}b zQXU$Cg|c5#yLDI8h%T}lcFXJPRt#o?r!otZr#wq5PWPG1iZo@{JoO}d_1V=rs>PBJ zg*tDGKi_;w`$9ce4RUJ!oxhR0xn|W!2}@10Rz`N+7KCymaJ2Z-9NmDCWbh3weWveQ z==KE(i$gJb6CXeTynAy}yajvcEU+MBxudPMYI1x|CfJL?Yw;LlZLe_cBua4W?xGK%CW|A#M zS&kLK(2kqXt>DONFrd*(wJ;+iSZ3#@F*QAb$*D=YoW%6Z3}*9r6eyA^m#d2GDY`Aw z`(G^0(EIDE!HzB#b!%U&R@I#PtIM1!*Ht}U&{35kt@Dq@d8^mh^tOg|dv8av-hDMk z_6{6L1|CDTU^3%a;;l(&Cx~6I6*7%NbfY&yJ7d^yvwlI5UBlk5Ax=^*QXiVgt5r-Yn4o|6N4a zve*H`?~LL3(ZM&;!qt*!N2w6i&_Jr$WH%x=GKOwNP-hFq_>t#O$j^9#D#iW^5OtoU z+t@Lk-=k_7rNS)cX4GWoIFFxakxMAFVE@%(a`yAdYw;SEU&7wx8CX{bTSnDFaje^U z0}5T3dGCt_f6?@wd}guVElOCes0Fzvt4CO!3lnWIakbOhX)UJ&Kej7)cd9*e4#wr% zVwVJ43m&+DUaKNMSHN6;j&}M}x<)mFlV|5JMS<#TXQuG-(NjoKC~-|Jj**cOina=< zlq;IFLPCU1RzN1@=(A%ohmy?e^paztZ*&);iSDgmjSdEnGETnsq8JDp;$gzlVXcoO z=qML4Gc``p^D!rbp&PO|=EhekAYI=LeXbVUNoDwGB~H>a$d z*b#%3f%ILJlodA)D@lAvo})3&od|K>4xY(T2DJ8BoT^4CB?_B0j95wp~iOb`6>`u%^=-3fYB|R#jj5_YV*CCy#mek`9$Tv z?wt!g>#E6m$&3uKeUVk&)~yG1WyZ_VAFcOSyyYGgx^PJyN(2{M5WcWjI*@U}(@$^s zr-Bd`<6}G1`1r&$dgyGJXp0GlqpfU<^+z6kvR3f}W5M%JXI_3Br(bhGe4<_C>Si{`0RdB(zveIoOD})OF)>qJ$Po)v10~QC2KF zh~X=*!$cQGR~u5nqbtJaF7(R_5>_^weSp4b!Mp`2=iiI6x(8;~Rx8mKliri#YjeB| zi!FlIukw>;asPe)Q%HlYm&z2?6ss&Jw^5Gw5kd9Il-LX{jB|w|UV7y)j=p&eGt<*3 zmzr0kEgRP%o$Z%sfV~IV{MOW|cmd^9pK26VlW9F$A4o@$itsdfDHR+QzpuElD@NWg z1dgm&&K?AHikSKmQEb?@6SGrisTNq&jksF1Co0a68r3qm34KE&=o?svOtw#=t5__d zeFqngt{RudtztegnO1{&sX(XM8J^uQyuK@&8@~B`Rpi)k?VXtGa2Jb(z+#J_^(*>Z zA5EoF_g&~d7Y(rK4gC!5NPZmw(2KIV3ntoPr;J~}aTqs@EKL>ndxs`*vamK4f*;!z zJXQMmKmIkwC#Fy)a>yfvXmSt^FMNq4`r=8dTNQBR_nVoXX-Xp*uv7T1 z9ouM;4v9cR%x_~cuaQ`G<~G%09NixjYRqb~MFwA&bYNZk6xwwMRy2;mEqgq*Vz;4{ z+Eww~F^p{4ioW4Nl=DTDiY2NAmg&7KNnjd|#1Q2Hnrw1Q8HQMbhhS#KT$YC;dP+{+ zueHz!kp^Ko`Zi}?WVLw1ut!*RLuj@aZSf|f5>8-r_f05vVa9!HrlsiWGZ(wVMG1@E zyx*tLm`9HEqO3Izae#mDG&ZCo5^-Hm#I=wxgKJhyRvekFMYJMiXFT=ni0A z;oN{MMpHAhI83R&<0nt!6p_{V_&IH}vpBX5WpVY^5t$xL%b5&!3{zAVPqY^-{zu0REnfrcVTA>Be-xK|6D7($<)^N z5mjA_E%mfJiW(lxWK?0FJ)Auo3Dr<5R@??%$tar^s`x4Tau(_(b>xeJLc43?z1 zp%d`ApoGzb^HV>C!>=C3G*1y$+is~bG3jumWYGc@kvx<| zQ4sC^iM&b_8P%NTnMyZbwFS5AyGqn)l7{jM?pz6g8mk@2cw2CHxrC z{>>QOa~HZ1twvX&;)IO4X9D8%o@y(o>oM~+9qI5n3ZG}$w2+8mA*kV)3(vfAe9gfN z)vlTxE$sObRvp9F+zux*O5}yELaWggZ4f@Sl>04-uvmN%RN~I)MOi&`8(N93zWK<* zc=qup)(xdW)R(omZGa#5h03xU~H_kZ=i8$r*}-L^qNw5 z^(NVQGf}B=E&iX)<#|pQj?;A6NCP%}9o3ZqKJjpMw3oTP;`}~5P?&Hsd$W1 zRE@9OOtFk(_8cur>I)vGzj@n@d$4zGBT}h^GzuqE4OyHh0;N7x^XgbNEnlzO%E^L- z7OYO>D9hBYQX|{3Y2Q20jbK$^YthwNN`soQE0&sf)#w0FsXF;t&7cZN-0JG+)f_5i zXoM3+1-JoWA)ek4nxZk+^{khggcT;b^4n$Am9eclsxsq>Hydoj6dB%zVwXwTSryn~ z?P^KU6=TVS)r+!vSnK$uYlAyLJo&9h@$yrTp+6n>(szcOvdK7d$rxS6C@mO4E)|u< zuTZMt)zdS0ZCv%Raoy^BZ`y-*+^`!%*`yeMt}kT=-G3$TsX4Skh&VGge26`$_g%SQ z2w}14!gG3Wz?d7)m$e(R@}~hEFAZ4tRGihiHsGz6Q47T|cJnKku0%KG3~s%P2MLq~%ntDvqHjKBGB{}(n6K$Mi31rN2NhKkxnC(W?6d*VLE>jCEKpr6u3&&$3zs?qce4`QSP@;WYr)nTgld0 zSW#1HKz7w|w4+6xB4V4@QuB%;fWYvPJMWE=45$Ge)a+I|zjjx_4 zBfo}~Wj%B+SZL9TXW-H24q+&N3in*C2e59SPrL_<#e&qoSacDlzjJj2CH+Ij5LKrHWl_|UXif!I zMH#3@VnmaD*z%U|r_1j14P>Hf$usb!XYurx{{~hhrqq5s?OB`Gh7gwbm0JXyu=W%T zD;Irf8=&`I?#Gu}sF9+aWV$bhTz@~Ji6k7JKSn$jF^0IJ+-wMA=0{fEEHa$YEqZUmE6 zE78^AhwevqWDNC6QN1rzC#;srA{XZw@^-bRo39<&9B`x+(VGb-Gl^I&7a`hY@cD2k zA`5XHGL^=_&=59k--`8DY=@Q0(RrRXZe(6$r5d)4iEY)ZGuoo!tg48a&KV!|YF?u4 zeRur;YVkq1UDk`wLW@?SD|)Yf8UYdbW6^4q^}+2)Janeq6JYevQDDR7R)iMg-}xr$ zXHH85#o%JP_|);>MQ%pb4i%A5$RjKT9s9@d)*JSqKbIBHIwl?V9W?W=xGzXq zEE$=)o;=$my3jpWRb2z&Mp&ZA%%-cc<+}Hx8$sE)mV_2(UwHwXM_a#jtFPzDR@PN4 zOhHZ6r09RPqZ+Wmhi>?d(52-nyQUCD$(HnDsXjO+Ek2huiqqmmUeZ*oo!=dwDzJF~ zqfYs0afRo;UQ@kRwW^uFrEc4>?{0)Q?nSw!7N*rhI?|eqo$xu@m>ohuv@~CWPTt4p zVn$ch)Wsp%={*W#a}v8Y!YscGe(aYv9SfCUzHbz9Xeep--` z+qxfDzvmNOYr(XALu*p*W6ynu(w_;09lu4AA`(?!GR_pPlC{q(a=D>~^^(-Oq(&u* z3^|7>N)7g}A4Vh*_sGfb@SU#%=!oI9twVRaBgE$*v`Rn{ma#e}Ne%2Di&h z19uO!7Jw|Z?`l3;A}rR-&&|!T$Kc0<{2ixVda1!4ItBb$Y$X*q^VH)AbFC@XFvsOZ z&DQSQ3|`dYvJ6TRP>L>;!=CzrL!i5_`0M5 zZR6nWv+?0HApcH`c1sml>0xZY<6j^(xCz~eR#Sl!Z@x_FzZtQm;<_JKtcZc2xXY_B zssA#`SVlqE+>C9x=6)1`@vZbQ>0Ac6fdSb_MA#*tBPz2Q&|(YIXJw))yQWir^{OH; zk*wFm!J<~FOB%4ksX$6yvW(-p5BxjKA|nmu-GnWfdP}K5EYa0sA+$WfkG?RUq?7nj z1n?MS^iqR8bOKmtac>JkiyB2j6Zii!74{<%#Ymco7Ac9U=!iucUn#DI7*gXxzEiV= z*dkU$)lBGbd0ad1zWZ{0^t-4mxV(r~#ic^cYElcLQlJ?NAE{fF%P1C$L|j$4kp#Bg z{C*5>-G^?35?UZIYUKjXz5X0hxiqTf3cV*KstGw>KO7}xwV*Gg)qR{aB_<1-{wZil zx{|NUox((%l$%UvvvmHV+$7hC#PonnOIhW6=~?6dXV)5jfHk|OXI!yly{hf9xNcRV z%ZiLV+4=6DfYZMX^)B=aY_UoBXi#OKA1xD>_84RVMxd$0j9zN6ht&j&fIf5U09wJB zfBq_5B8mvrnY;mjyo;s<$+a8Raw+OlgcYWUN$Mor?W5fiSc%3Pd5)|&rI(|HF-ecd zwJTP)`NvZze>puWq^2eomuOM0(t^ixwqy~cukjVPe;8voeV{8-fy@UxHn)}voI3U* zy|XqEOhSmpiiJfo&N6c8S*zE))SzX?gh+DM(hb2-ky1!GVMOCmnL!rfCU15r;SEH{ z@&YwJFJisLDM7p80chF{3}M+$qRpGfO{K?NAS)L;**iJ!VU+fpj9GC6_ir=CEZYERK* zN)|Roy;M>~cUMJ042x4!VYTo{bj7_>Rn-v8i`=QRrzIN5Wpk1Sj7Y?03Hh1(xSFs$ zIkb+fxUslIq{&CMT1RH=T5LXWk4)6Q6TgD=DWm6DtE;EsN*Yf)E{SkZVSaG&Kg-b?vYweN-M%*_yPtzlx)+ zN=4VLC=FQ3%l$|8zXKa@`d&;|tgf4NwLRN%&(+a%I{i6Z0xpTLG+3JW6as=%gQtpi zuc5ey6~zy1Pqm~9WIX(t-$EZPJjq;6itna@lxtL?Nb_$x+2=MM6S89PWh9k`H9Lcu z**R1aHT3uQ$?(8vL{GNmWEHQVzPVSt;2OInC0nmZw8iPbTHQscZv*zc^T(!uGD0)5_9Q|tlKqJ0Tq6;;|k;6RGhZTlW1wxBw z{8NXGDo~{Ys|~|it=_E5@I@9^*b+sFV20CxWp&gj>Kom6H+H@AUW`{mT}M_tMYu&( zAisxq;}UVngvHXtRsRG2tz0_b2%MVqz3O@Fpap<4^3dheJMJ~NTgcP!X zMYvEx7jBHAr>fj;n+Pl1Fs?;{F3XNeEfKiKDl?QtjA~rbh^l9qNx`fFQ*I61P*S){_p)c^o@4!oU73mJ$dI&z6zUaLH?2V>N=6QFQqCG6(?j} zt|eM}rj_G!SJ#3-t^AY5VM{j_>)Q_Xu{Af$Un*|sg?jaydEg=)_P6=)g-2?Bl776}YD^8!o zv;X`z$lFCq;S^;-;)n_q;UJ<4@+`1G$hWxFe1(a5nYt*YnxtOEsjQGnP5SFrvhe9* zKBxbrMp9ST*%X%-K`WlcRd?UpwPa=LR^b-D`mC;-IsS5E?v(Kf)CY@SSkcFJS-c^L zhB?ir=3R06P|d!Idbi0X-zThRVVT1xUw3>03pH0u5t2k!oVL|bmg%vQNSHSmh+q~)0TP4t{Bv?vq`$kW07J9c~k`@Z`V7~b&~bSv1{HrC?mbMo2$ zB^w5Rbj2{ykRcg{e@bxUXC-Dg!INk%q8O@*-ycU%Mqq)e`XYlo&tD6cmXiJ1;K&15v+6mr8R}UZ{xF_aR$;MZY zUp~`qdsu$3$fA{gD@J*C5=XxIk8+(CJ8mAGacd}1swYDSlT##_^hjs^yd+n9{ux4Z zs!EmNXgeTpC(nx`r?a#UfSnPP-4gd&RU{QCX~0+(+YjE0^}Fvtw}M3$t=6ror995Q zdPq!zl}SY-UFWZ3`Rh)8M+$~|-!04^ZSTL{7!lGK58~7tgBR;MvU0U_UQ)JVv&4^kYX0gEhJty^&_FqKMu5^Y3V35!7=oDa}RX4PPzhGGV% zZ4S@Y<>;Y@O9lG`{@bgAPY`C*X{Y?eU;P)g0P+pSwkI9IOreYcTA*0Ajzq%yo#nUe zVYdWY$ilSy=6)=3M4+FGL_V_1mz}&Xt>vojAik}pQ%=%zRVpH%FTy3V+IHIyW5eFJ zoi~rETfjbnn}*R6>{mjeC_wdxbdchr$X+7MMW6w@x8cHCU^D2kd;s{qlcv&ewPx@rtowo4UJ0~Sjqlo7|a+kaU1TE)Ny4LQ!nI>ton)&;O1Yi^!@=1<lI zBP<3t6wl4gjkO|mH)^hre{-rQckiJkFj?_us8x~0(Z|1mBaeMuxfm4ZoBUv}y4&yL+kL`wIO+L{xtgsk1g>wIkXo!E+C1=Wuvlgf%U);G-7p z!~XC2cUZscR&*tH_OHbhV3y&jY9>BGu5@aR9+3y)uZhF12SzxXhsRh8w;*iu^Nygt2%uKhIe4^+dhQYrv1oQ(3SNn29p&l z0$UYX(EIrr<^U`MmW8kwEOn2^HZ7~vt`Utx-QGZ79|y^KBp{)=3Tj&F6`>Z;gnU4q9vYQ8%jmgj3z*S<^@*u zTh&Em!D_$?*Sj(saP`}M9P4&=ufXC`$*MRviI>0gH6f-Ly;q3P`x#-PX9o&CH|!AN zsQxM5EJ$mC&pkqATRk+pT&SX<84uz`U9J{$=wX>&L$Hj7VbMiJdaLT?g7Po4V%U81 z2eIvz??K*9Ue;u_9Ca%{mW{Aj>Ru|9?xqhxOZBWCY4s2kmr7Rosk3QWcHE19*%`EZ5Pp3Ns1#oM_yqqQ%~4fVId@Og3dn>9B)w$nEOjlH z<}_eFUqGo?L3;a5xaOXJgVewVbSo~Etn$+nxc@IcO%ZGviFgVw)e9pO^+a4H`&Xk} zcq0+^iW`<|omb;qjIdPN)@@D?iV1E_t2KQiLUdHcDnK!>Epv3`ATzoH*Szyb5n6W@ zrfYhNNtdD>WW{wWI;OEqST@39=rJT8Xr(`k(Ic%MmWoRyE5^fr`A0Z=@=a+b<=Ir+ z*em;W-J4?y`|PM*jyjm3RG;)>#Y0HcRqvIhn~3YQphoYj6Bq1R zcwq)HvZ}05qB>VNiP0PH!BubjS5&WRO^V+MxKy%Y9D4NYm^^g?(Rc(-RgIdDal&VdPk)-CtD{&(tcctwpU;1y6<2qI(Uz2@*TC3IkQPQx^Nkl~aW-B+GMkhdi7hF>ia4$p$~B}$cVWk!AI9+Z8@g6p z!Dt6r9WO*MF*Ao#-@Xs$o_m-|e6*OxVn`&?h{cj3ei4U?xN@lo`8@wZx~FO(HKh=h z<@luQIu+=p|I{E(J~tIg{jpf4E@SMqy^>wUx;=Me!)-qRH#35&ZFO~2#b^gv5kA!} zH;W5l#q_ZhY5jgHA`F8bY4xxW+Cf&wPR!u`zx-pIe1>Wzc15gv;yh+TYqpE96hUh*&sE8;T?mS3))x(P&D} zhjPUaYip*+qJ+h8P?xM=lS;5|d!y{_n)3aVUN|N!j;!c3rZ(-v_FLbJ_~>qwoJiLb z6{DNTim{>%9TaKZA9NInA3f6Q!H;&3)l9*~kyDWI6H~`uz>#l%0q0(S9HDv{Q68Hf zvXnPnRFM})V8&TpcH)&9#$1ZBC{@LzB?1iHkHw_Ki(!dsQ*V|gaIAbzd<09B`g223 zWH#)=)*Ii8k)3Zryi48I{b&bSO_W1;X}X!Td6CLc>Tl-NM{u0ZPi^`*r3B+dSZPWZ zCS^kr;bEEVdLL0ffR;F~8=;IyX1vGaPA-Y8Od3!;0@<}fkyBmfR`K|H`q>U4xrrj* zgC9nGf0wG&Yp@I0%XeV4Z3H;TaJXb!&M^I{dyF4FqkD+PS^)dG6Be85Jwt$G6* zT#T&zP{ny#;!1O`Iy zfyCez+Wn^x%M56P)$Wg`{9q&7R=oiYf{>MgQg5yVRw+^|{?daPuhWI*4%MxLg97R%aySM50Lg3UNj|jO0*LA^al9fC^RuSzYt)3WT zZKECH3PwB0>T*Uq#1)Kokkxz`M<$NGn51fY6bK6}L_sM1bumf{S{^yMYmuRRXp^{N z)yYa;16CRBBCUMQ#b+r+*sE);EvWGzsY?=9FnVOw9+>eU;Y$o4mxHW&I3H#_$fbxY z7@a^?@;cA~XcuWQc%IfDypZpu2)i7d0_1wt2Z^vQ6+N=LbTC12y=q-xleP2FBdg^H z6Vw(Ws*R~jO93)Eg{^>VDP+ayxKEC5F8(ZCeLva&X*7Op28ZW*t5zqYolzAdUs7b% ztpoH{8|{p$7&BFNu!t<$6x_q5p`B3`gNW(@BB~FpWK-}`(6OJMxw*MdhC-oVN1MR4 ztXRT|UI7OTp>CkL^s!WxK$sKTZL1DzJxO;Pf9y zReaelaO#g!fSd~Kp}pYrA4gSu*+xt|j`M+RHv0fN7+sN2Zl{9Kk6xN^Ww9JY#po#k zTf=e?6{DvFTplbFQFWXW@IvUCPZWzWDAM}mrOeb?3_apnUMv?;F}PlJdJ;WxMF1=p zQ8Bn)#j~t>;)?cRxrmCv^(vla)oJ32#n5%1Dm}fJ>soCr3oG=9tG!rGqGEzNJ=v>Q z1;KI>6%*7kBC1}!s(n~aq9RNaS$(RL>s1S(E4M+*l_E6Y+Sccfl<@GG3VH~RRb zufHl7mXoL$z5c59Vws7mBm1kCimv{IwOd6vA4XNk1lEhXE)15Z-?KR>v#NNw;&P!! zR!ajb{RVS%WGxx4xLoLwReQinzjX-*>r)QWk>y)!I$5&56m!||ggQKXvv?XK}UO|E0*;1H}P)`*Y5FAAf5>buN z(N1~#e6X;>w(XB~x(e`OSTozA<&L%tIO^gVTufevXD`ofK9kgSgHe3&isXut)I#7$ zYbLMAt8|%WaG9hsN!9EdUS+=ENNb`J!da?iarD*0`CyXD#awCjT~3lR=r&=+g|MbR zQ7aW~85r_<=uCCZM_z`c?xB?5XsR;{>xR5$3Tr>|G9<;3RIEcyBn){?mV1$xAt@eD z-!A*4i-GNx9LM2C;6qp=Sd&|zd_I3KM_VgJTnr|!LlZSTL?lJCX_tCNCzf$%=)1EwtuqUK}|M z#vPUNi@PflN%_!~3c#gc&3Opi643RN$aU^z%Qi+(FF@zHi ztLTU?g`DX8@FkNH6V>HLOhM5gE=nsp9F1A9U3e+QvcBZoS|v9oo3Q>z$7k zLGOHY7nXd!);fBfeH!|U`F?hTUr9Oq8VAqVVJn#552{+~osVTf?|gI@T0UQE6+OaQ z06a7@91io2^8M%`Fxs8A=$(&CL+^Za7cTd---Xa4tP4QzDmjc5aq zr$4t`yTRW1SUP&=qr0#Y=W8vXM_5aQNr~uOe(%c(ZzuR*Bog`DO80;C&d2%aosaIq zYM!rdMUSvrfZ18n)sJ3gDynIorPFCU!QT1kosTX-N1m^4LXWW804)LLeEzj-?F*_? z6;D&4{XRC)?1nE{?|gI?*7Etd7`pL%buxN{)n?EKv!7ixLZQ$>s4wV&E>cw;rQ;C2 z-UsP8v=+W#z4Oso=<@S%F&xDjJ71lK9$~Ej%$JnEoOE2vUr@eIzv{>8fWJgXp}t&C z(+=<`E0QjkFIVq;bQU`Bd|V8d_4!&&^ayJe!C%xup};e|`t_HUzo6Ub=-0RL&*n=z zc7cbQ)`wiDzvW!{QT_89^z%{rQccsY_9*4fkM_P;3!`^FIty()9~Z-t&Q~-VJ(Nf! frhDgW_3-}z;}96=%ak`500000NkvXXu0mjfSzwGU literal 0 HcmV?d00001 diff --git a/client/src/assets/telegram_avatars_cluster_2.png b/client/src/assets/telegram_avatars_cluster_2.png new file mode 100644 index 0000000000000000000000000000000000000000..599b7aa4b349c40b8e6869b66caf7bab9933de72 GIT binary patch literal 51677 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 O0000 { + return ( + <> + + + ); +}; + +export default CreateCommunityLayout; diff --git a/client/src/pages/communities.page.tsx b/client/src/pages/communities.page.tsx index 42b7d11..2cfe80a 100644 --- a/client/src/pages/communities.page.tsx +++ b/client/src/pages/communities.page.tsx @@ -92,16 +92,15 @@ const CommunitiesPage = () => { >
    -

    Чаты

    +

    Сообщества

    { - openTelegramLink(`https://t.me/${BOT_USERNAME}?startgroup=`); + navigate(`/community/create/initial`); }} />
    - goToMyProfile()} />
    { + return ( + +
    +
    +
    +
    + +

    у вас уже есть чат в telegram

    +
    + +
    + +
    +
    + +

    сообщество без общего чата

    +
    + +
    +
    +
    +
    + ); +}; + +export default CreateCommunityInitial; diff --git a/client/src/pages/editProfileCommunity.page.tsx b/client/src/pages/editProfileCommunity.page.tsx index 9296929..1a9fbba 100644 --- a/client/src/pages/editProfileCommunity.page.tsx +++ b/client/src/pages/editProfileCommunity.page.tsx @@ -4,7 +4,7 @@ import DevImage from "../assets/dev.png"; import ButtonComponent from "../components/button.component"; import InputFieldComponent from "../components/form/inputField.component"; import TextareaFieldComponent from "../components/form/textareaField.component"; -import { useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { Member } from "../types/member/member.interface"; import { useState } from "react"; import { Field } from "../types/fields/field.interface"; @@ -19,6 +19,7 @@ import usePatchMember, { import { MemberConfig } from "../types/member/memberConfig.interface"; const EditProfileCommunityPage = () => { + const navigate = useNavigate(); const { communityId } = useParams(); const { data: userData, isSuccess } = useGetMe(); @@ -70,6 +71,8 @@ const EditProfileCommunityPage = () => { }; console.log(JSON.stringify(newMemberData)); await patchMemberMutation.mutateAsync(newMemberData); + + navigate(-1); }; return ( From fb7b66358b346f3cd5fdcc8dd1c748177691b30b Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 15 May 2025 04:06:56 +0300 Subject: [PATCH 32/78] progress bar boilerplate --- client/src/App.tsx | 22 ++++++++++ client/src/components/createLayout.tsx | 40 ++++++++++++++++++- .../createCommunityInitial.tsx | 30 ++++++++++++-- .../communityWithChatDescriptionPage.tsx | 7 ++++ .../createCommunityWithChatLayout.tsx | 12 ++++++ .../communityWithoutChatDescriptionPage.tsx | 7 ++++ .../createCommunityWithoutChatLayout.tsx | 11 +++++ 7 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx create mode 100644 client/src/pages/create_community/withChat/createCommunityWithChatLayout.tsx create mode 100644 client/src/pages/create_community/withoutChat/communityWithoutChatDescriptionPage.tsx create mode 100644 client/src/pages/create_community/withoutChat/createCommunityWithoutChatLayout.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index b5f703f..9ba4a9d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -18,6 +18,9 @@ import InvitationPage from "./pages/invitation.page"; import EditProfileCommunityPage from "./pages/editProfileCommunity.page"; import CreateCommunityLayout from "./components/createLayout"; import CreateCommunityInitial from "./pages/create_community/createCommunityInitial"; +import CreateCommunityWithChatLayout from "./pages/create_community/withChat/createCommunityWithChatLayout"; +import CreateCommunityWithoutChatLayout from "./pages/create_community/withoutChat/createCommunityWithoutChatLayout"; +import CommunityWithoutChatDescriptionPage from "./pages/create_community/withoutChat/communityWithoutChatDescriptionPage"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -61,6 +64,25 @@ function App() { element={} > } /> + } + > + } + /> + + + } + > + } + /> + { + const location = useLocation(); + + const steps = [ + "/community/create/initial", + "/community/create/with_chat", + "/community/create/without_chat", + ]; + + const activeIndex = steps.findIndex((step) => + location.pathname.startsWith(step) + ); + return ( <> + {!location.pathname.startsWith("/community/create/initial") && ( +
    + {steps.map((_, index) => ( +
    + {index < activeIndex && ( +
    + )} + {index >= activeIndex && ( + + )} +
    + ))} +
    + )} + ); diff --git a/client/src/pages/create_community/createCommunityInitial.tsx b/client/src/pages/create_community/createCommunityInitial.tsx index e593950..805b902 100644 --- a/client/src/pages/create_community/createCommunityInitial.tsx +++ b/client/src/pages/create_community/createCommunityInitial.tsx @@ -2,23 +2,45 @@ import EBBComponent from "../../components/enableBackButtonComponent"; import TGAvatarsClusterFirst from "../../assets/telegram_avatars_cluster_1.png"; import TGAvatarsClusterSecond from "../../assets/telegram_avatars_cluster_2.png"; import Nav from "../../assets/navigation.svg"; +import { useNavigate } from "react-router-dom"; const CreateCommunityInitial = () => { + const navigate = useNavigate(); return (
    -
    +
    { + navigate("/community/create/with_chat"); + }} + >
    - +
    + +

    у вас уже есть чат в telegram

    -
    +
    { + navigate("/community/create/without_chat"); + }} + className="bg-[#F5F5F5] flex flex-row items-center justify-between px-[24px] py-[24px] rounded-[20px]" + >
    - +
    + +

    сообщество без общего чата

    diff --git a/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx b/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx new file mode 100644 index 0000000..1b77bdb --- /dev/null +++ b/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx @@ -0,0 +1,7 @@ +import EBBComponent from "../../../components/enableBackButtonComponent"; + +const CommunityWithChatDescriptionPage = () => { + return Community Description; +}; + +export default CommunityWithChatDescriptionPage; diff --git a/client/src/pages/create_community/withChat/createCommunityWithChatLayout.tsx b/client/src/pages/create_community/withChat/createCommunityWithChatLayout.tsx new file mode 100644 index 0000000..2b27d6b --- /dev/null +++ b/client/src/pages/create_community/withChat/createCommunityWithChatLayout.tsx @@ -0,0 +1,12 @@ +import { Outlet } from "react-router-dom"; + +const CreateCommunityWithChatLayout = () => { + return ( + <> + with chat + + + ); +}; + +export default CreateCommunityWithChatLayout; diff --git a/client/src/pages/create_community/withoutChat/communityWithoutChatDescriptionPage.tsx b/client/src/pages/create_community/withoutChat/communityWithoutChatDescriptionPage.tsx new file mode 100644 index 0000000..d8f2ff5 --- /dev/null +++ b/client/src/pages/create_community/withoutChat/communityWithoutChatDescriptionPage.tsx @@ -0,0 +1,7 @@ +import EBBComponent from "../../../components/enableBackButtonComponent"; + +const CommunityWithoutChatDescriptionPage = () => { + return Community Description; +}; + +export default CommunityWithoutChatDescriptionPage; diff --git a/client/src/pages/create_community/withoutChat/createCommunityWithoutChatLayout.tsx b/client/src/pages/create_community/withoutChat/createCommunityWithoutChatLayout.tsx new file mode 100644 index 0000000..f84b39c --- /dev/null +++ b/client/src/pages/create_community/withoutChat/createCommunityWithoutChatLayout.tsx @@ -0,0 +1,11 @@ +import { Outlet } from "react-router-dom"; + +const CreateCommunityWithoutChatLayout = () => { + return ( + <> + without chat + + ); +}; + +export default CreateCommunityWithoutChatLayout; From e8dfe80dd3975174a86c99d8eba912906c5e9334 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Thu, 15 May 2025 04:18:35 +0300 Subject: [PATCH 33/78] fix lint --- client/src/App.tsx | 2 +- client/src/assets/add_blue.svg | 3 --- client/src/assets/arrow_left_24.svg | 3 --- client/src/assets/tg.svg | 3 --- client/src/pages/communities.page.tsx | 8 +------- .../create_community}/createLayout.tsx | 0 client/src/pages/registration.page.tsx | 9 +-------- client/src/utils/fieldsNotEmpty.ts | 2 +- 8 files changed, 4 insertions(+), 26 deletions(-) delete mode 100644 client/src/assets/add_blue.svg delete mode 100644 client/src/assets/arrow_left_24.svg delete mode 100644 client/src/assets/tg.svg rename client/src/{components => pages/create_community}/createLayout.tsx (100%) diff --git a/client/src/App.tsx b/client/src/App.tsx index 9ba4a9d..f5738d6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -16,7 +16,7 @@ import GeneralCurrentProfilePage from "./pages/generalCurrentProfile.page"; import RegistrationPage from "./pages/registration.page"; import InvitationPage from "./pages/invitation.page"; import EditProfileCommunityPage from "./pages/editProfileCommunity.page"; -import CreateCommunityLayout from "./components/createLayout"; +import CreateCommunityLayout from "./pages/create_community/createLayout"; import CreateCommunityInitial from "./pages/create_community/createCommunityInitial"; import CreateCommunityWithChatLayout from "./pages/create_community/withChat/createCommunityWithChatLayout"; import CreateCommunityWithoutChatLayout from "./pages/create_community/withoutChat/createCommunityWithoutChatLayout"; diff --git a/client/src/assets/add_blue.svg b/client/src/assets/add_blue.svg deleted file mode 100644 index 5dc2088..0000000 --- a/client/src/assets/add_blue.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/arrow_left_24.svg b/client/src/assets/arrow_left_24.svg deleted file mode 100644 index 506957c..0000000 --- a/client/src/assets/arrow_left_24.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/tg.svg b/client/src/assets/tg.svg deleted file mode 100644 index 4ad14ba..0000000 --- a/client/src/assets/tg.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/pages/communities.page.tsx b/client/src/pages/communities.page.tsx index 2cfe80a..1198037 100644 --- a/client/src/pages/communities.page.tsx +++ b/client/src/pages/communities.page.tsx @@ -1,12 +1,10 @@ import { useEffect, useRef, useState } from "react"; -import ProfileComponent from "../components/profile.component"; import SearchBarComponent from "../components/searchBar.component"; -import { openTelegramLink, popup } from "@telegram-apps/sdk-react"; +import { popup } from "@telegram-apps/sdk-react"; import ChatPreviewComponent from "../components/chatPreview.component"; import DBBComponent from "../components/disableBackButton.component"; import { Outlet, useNavigate } from "react-router-dom"; import useChatsSearchStore from "../stores/chatsSearch.store"; -import { BOT_USERNAME } from "../shared/constants"; import NotFound from "../assets/notFound.svg"; import AddButton from "../assets/add_green.svg"; import { motion } from "motion/react"; @@ -36,10 +34,6 @@ const CommunitiesPage = () => { navigate(`/community/${communityId}`); }; - const goToMyProfile = () => { - navigate(`/profile/current/`); - }; - const deleteHandler = async (chatPreviewData: Community) => { popup .open({ diff --git a/client/src/components/createLayout.tsx b/client/src/pages/create_community/createLayout.tsx similarity index 100% rename from client/src/components/createLayout.tsx rename to client/src/pages/create_community/createLayout.tsx diff --git a/client/src/pages/registration.page.tsx b/client/src/pages/registration.page.tsx index dd993f4..ff9d494 100644 --- a/client/src/pages/registration.page.tsx +++ b/client/src/pages/registration.page.tsx @@ -4,16 +4,9 @@ import useGetMe from "../hooks/users/fetchHooks/useGetMe"; import { handleImageError } from "../utils/imageErrorHandler"; import DevImage from "../assets/dev.png"; import { useState } from "react"; -import usePatchMember, { - PatchMemberArgs, -} from "../hooks/members/mutations/usePatchMember"; -import { - fieldValuesToFields, - fieldsToFieldValues, -} from "../mappers/FieldValues"; +import { fieldsToFieldValues } from "../mappers/FieldValues"; import { Field } from "../types/fields/field.interface"; import { MemberConfig } from "../types/member/memberConfig.interface"; -import fieldsAreEqual from "../utils/equalFields"; import fieldsNotEmpty from "../utils/fieldsNotEmpty"; import ButtonComponent from "../components/button.component"; import InputFieldComponent from "../components/form/inputField.component"; diff --git a/client/src/utils/fieldsNotEmpty.ts b/client/src/utils/fieldsNotEmpty.ts index 961593e..f741fbf 100644 --- a/client/src/utils/fieldsNotEmpty.ts +++ b/client/src/utils/fieldsNotEmpty.ts @@ -1,7 +1,7 @@ import { Field } from "../types/fields/field.interface"; const fieldsNotEmpty = (a: Field[]) => { - return a.every((field, index) => { + return a.every((field) => { if (field.type === "textinput") { return field.textinput?.default.trim().length! > 0; } From 6828202b9b040d96247580babf41ef8ecc4cc70d Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 16 May 2025 15:02:20 +0300 Subject: [PATCH 34/78] drag n drop boilerplate --- client/package-lock.json | 14 ++ client/package.json | 1 + client/src/App.tsx | 176 ++++++++++-------- .../assets/fieldIcons/textarea_activate.svg | 3 + .../assets/fieldIcons/textarea_deactivate.svg | 3 + .../assets/fieldIcons/textinput_activate.svg | 3 + .../fieldIcons/textinput_deactivate.svg | 3 + client/src/assets/plus.svg | 3 + client/src/assets/white_trash_bin.svg | 4 + .../src/components/communityProfileField.tsx | 122 ++++++++++++ .../components/form/inputField.component.tsx | 4 +- .../form/textareaField.component.tsx | 35 +++- .../src/components/sortableFieldWrapper.tsx | 31 +++ .../createCommunityInitial.tsx | 4 +- .../pages/create_community/createLayout.tsx | 40 +--- .../withChat/communityWithChatConnectPage.tsx | 5 + .../communityWithChatDescriptionPage.tsx | 35 +++- .../withChat/communityWithChatProfilePage.tsx | 122 ++++++++++++ .../createCommunityWithChatLayout.tsx | 52 +++++- .../createCommunityWithoutChatLayout.tsx | 73 +++++++- .../create_community/communityInfo.store.ts | 38 ++++ 21 files changed, 632 insertions(+), 139 deletions(-) create mode 100644 client/src/assets/fieldIcons/textarea_activate.svg create mode 100644 client/src/assets/fieldIcons/textarea_deactivate.svg create mode 100644 client/src/assets/fieldIcons/textinput_activate.svg create mode 100644 client/src/assets/fieldIcons/textinput_deactivate.svg create mode 100644 client/src/assets/plus.svg create mode 100644 client/src/assets/white_trash_bin.svg create mode 100644 client/src/components/communityProfileField.tsx create mode 100644 client/src/components/sortableFieldWrapper.tsx create mode 100644 client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx create mode 100644 client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx create mode 100644 client/src/stores/create_community/communityInfo.store.ts diff --git a/client/package-lock.json b/client/package-lock.json index 6bd0e1b..15ffc4f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -22,6 +22,7 @@ "react-router-dom": "^7.3.0", "shamps-tunnel": "^1.1.3", "tailwindcss": "^4.0.13", + "uuid": "^11.1.0", "zustand": "^5.0.3" }, "devDependencies": { @@ -4449,6 +4450,19 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/valibot": { "version": "1.0.0-beta.14", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.0.0-beta.14.tgz", diff --git a/client/package.json b/client/package.json index 5148513..bc14ac3 100644 --- a/client/package.json +++ b/client/package.json @@ -25,6 +25,7 @@ "react-router-dom": "^7.3.0", "shamps-tunnel": "^1.1.3", "tailwindcss": "^4.0.13", + "uuid": "^11.1.0", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/client/src/App.tsx b/client/src/App.tsx index f5738d6..21c6dc2 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,6 +1,6 @@ import "./App.css"; import { Route, Routes, useLocation, useNavigate } from "react-router-dom"; -import { useCallback, useEffect } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { backButton } from "@telegram-apps/sdk-react"; import InitialPage from "./pages/initial.page"; import { AnimatePresence, motion } from "framer-motion"; @@ -21,6 +21,9 @@ import CreateCommunityInitial from "./pages/create_community/createCommunityInit import CreateCommunityWithChatLayout from "./pages/create_community/withChat/createCommunityWithChatLayout"; import CreateCommunityWithoutChatLayout from "./pages/create_community/withoutChat/createCommunityWithoutChatLayout"; import CommunityWithoutChatDescriptionPage from "./pages/create_community/withoutChat/communityWithoutChatDescriptionPage"; +import CommunityWithChatProfilePage from "./pages/create_community/withChat/communityWithChatProfilePage"; +import CommunityWithChatDescriptionPage from "./pages/create_community/withChat/communityWithChatDescriptionPage"; +import CommunityWithChatConnectPage from "./pages/create_community/withChat/communityWithChatConnectPage"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -31,93 +34,104 @@ const pageVariants = { function App() { const navigate = useNavigate(); const location = useLocation(); + + const disableAnimation = useMemo( + () => + location.pathname.startsWith("/community/create/with_chat") || + location.pathname.startsWith("/community/create/without_chat"), + [location.pathname] + ); + const setBackButtonHandler = useCallback(() => { - backButton.onClick(() => { - navigate(-1); - }); + backButton.onClick(() => navigate(-1)); }, [navigate]); - useEffect(() => { - setBackButtonHandler(); - }, []); + useEffect(setBackButtonHandler, [setBackButtonHandler]); - return ( - -
    - - + }> + } /> + } /> + + }> + } /> + }> + } + /> + } /> + } + /> + + } > - - }> - } /> - } /> - } - > - } /> - } - > - } - /> - + } + /> + + - } - > - } - /> - - + + + + } + /> - - - - } - /> - } - /> - } - /> - } - /> - {/* } /> */} - } /> - } - /> - - } /> - } /> - } /> - - - + } + /> + } + /> + } + /> + + } /> + } + /> + + + } /> + } /> + } /> + + ); + + return ( + +
    + {disableAnimation ? ( + routes + ) : ( + + + {routes} + + + )}
    ); diff --git a/client/src/assets/fieldIcons/textarea_activate.svg b/client/src/assets/fieldIcons/textarea_activate.svg new file mode 100644 index 0000000..ad58288 --- /dev/null +++ b/client/src/assets/fieldIcons/textarea_activate.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/fieldIcons/textarea_deactivate.svg b/client/src/assets/fieldIcons/textarea_deactivate.svg new file mode 100644 index 0000000..f86fefa --- /dev/null +++ b/client/src/assets/fieldIcons/textarea_deactivate.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/fieldIcons/textinput_activate.svg b/client/src/assets/fieldIcons/textinput_activate.svg new file mode 100644 index 0000000..82d5ce0 --- /dev/null +++ b/client/src/assets/fieldIcons/textinput_activate.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/fieldIcons/textinput_deactivate.svg b/client/src/assets/fieldIcons/textinput_deactivate.svg new file mode 100644 index 0000000..a2539c6 --- /dev/null +++ b/client/src/assets/fieldIcons/textinput_deactivate.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/plus.svg b/client/src/assets/plus.svg new file mode 100644 index 0000000..41ab222 --- /dev/null +++ b/client/src/assets/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/white_trash_bin.svg b/client/src/assets/white_trash_bin.svg new file mode 100644 index 0000000..85749f0 --- /dev/null +++ b/client/src/assets/white_trash_bin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/components/communityProfileField.tsx b/client/src/components/communityProfileField.tsx new file mode 100644 index 0000000..6e04d14 --- /dev/null +++ b/client/src/components/communityProfileField.tsx @@ -0,0 +1,122 @@ +import { useState } from "react"; +import TrashBin from "../assets/white_trash_bin.svg"; +import { FieldType } from "../types/fields/field.type"; +import TextinputIconDeactivate from "../assets/fieldIcons/textinput_deactivate.svg"; +import TextareaIconActivate from "../assets/fieldIcons/textarea_activate.svg"; +import TextinputIconActivate from "../assets/fieldIcons/textinput_activate.svg"; +import TextareaIconDeactivate from "../assets/fieldIcons/textarea_deactivate.svg"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +interface CommunityProfileFieldProps { + id: string; + index: number; + handleDelete: () => void; + isOpen: boolean; + onOpen: () => void; + onClose: () => void; +} + +const CommunityProfileField = (props: CommunityProfileFieldProps) => { + const [fieldType, setFieldType] = useState("textinput"); + + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ + id: props.id, + }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + const getFieldImage = () => { + switch (fieldType) { + case "textinput": + return TextinputIconDeactivate; + case "textarea": + return TextareaIconDeactivate; + default: + return TextinputIconDeactivate; + } + }; + + return ( +
    +
    + +
    { + e.stopPropagation(); + props.onOpen(); + }} + > + Иконка поля +
    + {props.isOpen && ( +
    + {fieldType === "textarea" && ( + <> +
    { + setFieldType("textarea"); + props.onClose(); + }} + > + абзац + Активный абзац +
    +
    { + setFieldType("textinput"); + props.onClose(); + }} + > + строка + Неактивная строка +
    + + )} + {fieldType === "textinput" && ( + <> +
    { + setFieldType("textinput"); + props.onClose(); + }} + > + строка + Активная строка +
    +
    { + setFieldType("textarea"); + props.onClose(); + }} + > + абзац + Неактивный абзац +
    + + )} +
    + )} +
    +
    + Удалить +
    +
    + ); +}; + +export default CommunityProfileField; diff --git a/client/src/components/form/inputField.component.tsx b/client/src/components/form/inputField.component.tsx index a5f8b97..0378cbd 100644 --- a/client/src/components/form/inputField.component.tsx +++ b/client/src/components/form/inputField.component.tsx @@ -7,6 +7,8 @@ export interface InputFieldComponentProps { maxLength: number; onFocus?: () => void; onBlur?: () => void; + fillNotRequired?: boolean; + resizable?: boolean; } const InputFieldComponent = (props: InputFieldComponentProps) => { @@ -17,7 +19,7 @@ const InputFieldComponent = (props: InputFieldComponentProps) => {
    { + const textareaRef = useRef(null); const isEmpty = props.value.trim() === ""; + + const autoResize = () => { + const textarea = textareaRef.current; + if (!textarea) return; + + textarea.style.height = "auto"; + textarea.style.height = textarea.scrollHeight + "px"; + }; + + useEffect(() => { + autoResize(); + }, [props.value]); + const limitNewlines = (event: React.KeyboardEvent) => { const value = event.currentTarget.value; const newlines = (value.match(/\n/g) || []).length; @@ -15,25 +30,31 @@ const TextareaFieldComponent = (props: InputFieldComponentProps) => {
    {props.title} + />
    ); diff --git a/client/src/components/sortableFieldWrapper.tsx b/client/src/components/sortableFieldWrapper.tsx new file mode 100644 index 0000000..ca66ff9 --- /dev/null +++ b/client/src/components/sortableFieldWrapper.tsx @@ -0,0 +1,31 @@ +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +interface SortableFieldWrapperProps { + id: string; + children: React.ReactNode; + dragHandle?: (attributes: any, listeners: any) => React.ReactNode; +} + +const SortableFieldWrapper = ({ + id, + children, + dragHandle, +}: SortableFieldWrapperProps) => { + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
    +
    + {dragHandle?.(attributes, listeners)} +
    {children}
    +
    +
    + ); +}; +export default SortableFieldWrapper; diff --git a/client/src/pages/create_community/createCommunityInitial.tsx b/client/src/pages/create_community/createCommunityInitial.tsx index 805b902..6c76ea6 100644 --- a/client/src/pages/create_community/createCommunityInitial.tsx +++ b/client/src/pages/create_community/createCommunityInitial.tsx @@ -13,7 +13,7 @@ const CreateCommunityInitial = () => {
    { - navigate("/community/create/with_chat"); + navigate("/community/create/with_chat/description"); }} >
    @@ -30,7 +30,7 @@ const CreateCommunityInitial = () => {
    { - navigate("/community/create/without_chat"); + navigate("/community/create/without_chat/description"); }} className="bg-[#F5F5F5] flex flex-row items-center justify-between px-[24px] py-[24px] rounded-[20px]" > diff --git a/client/src/pages/create_community/createLayout.tsx b/client/src/pages/create_community/createLayout.tsx index 4739052..a986b99 100644 --- a/client/src/pages/create_community/createLayout.tsx +++ b/client/src/pages/create_community/createLayout.tsx @@ -1,46 +1,8 @@ -import { Outlet, useLocation } from "react-router-dom"; -import { motion } from "framer-motion"; +import { Outlet } from "react-router-dom"; const CreateCommunityLayout = () => { - const location = useLocation(); - - const steps = [ - "/community/create/initial", - "/community/create/with_chat", - "/community/create/without_chat", - ]; - - const activeIndex = steps.findIndex((step) => - location.pathname.startsWith(step) - ); - return ( <> - {!location.pathname.startsWith("/community/create/initial") && ( -
    - {steps.map((_, index) => ( -
    - {index < activeIndex && ( -
    - )} - {index >= activeIndex && ( - - )} -
    - ))} -
    - )} - ); diff --git a/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx b/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx new file mode 100644 index 0000000..8cf470d --- /dev/null +++ b/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx @@ -0,0 +1,5 @@ +const CommunityWithChatConnectPage = () => { + return <>Connect Page; +}; + +export default CommunityWithChatConnectPage; diff --git a/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx b/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx index 1b77bdb..fee04d6 100644 --- a/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx @@ -1,7 +1,40 @@ +import { useNavigate } from "react-router-dom"; +import ButtonComponent from "../../../components/button.component"; import EBBComponent from "../../../components/enableBackButtonComponent"; +import TextareaFieldComponent from "../../../components/form/textareaField.component"; +import useCommunityInfoStore from "../../../stores/create_community/communityInfo.store"; +import FixedBottomButtonComponent from "../../../components/fixedBottomButton.component"; const CommunityWithChatDescriptionPage = () => { - return Community Description; + const navigate = useNavigate(); + + const { setDescription, description } = useCommunityInfoStore(); + return ( + +
    +

    + Основная информация +

    +
    + + + +
    + navigate("/community/create/with_chat/profile")} + /> +
    +
    + ); }; export default CommunityWithChatDescriptionPage; diff --git a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx new file mode 100644 index 0000000..b312fd2 --- /dev/null +++ b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx @@ -0,0 +1,122 @@ +import { useNavigate } from "react-router-dom"; +import EBBComponent from "../../../components/enableBackButtonComponent"; +import FixedBottomButtonComponent from "../../../components/fixedBottomButton.component"; +import Plus from "../../../assets/plus.svg"; +import { useState } from "react"; +import { Field } from "../../../types/fields/field.interface"; +import CommunityProfileField from "../../../components/communityProfileField"; +import { + DndContext, + closestCenter, + useSensor, + useSensors, + PointerSensor, + DragEndEvent, +} from "@dnd-kit/core"; +import { + SortableContext, + verticalListSortingStrategy, + arrayMove, +} from "@dnd-kit/sortable"; +import { v4 as uuidv4 } from "uuid"; + +export type FieldWithId = Field & { id: string }; + +const CommunityWithChatProfilePage = () => { + const navigate = useNavigate(); + const [openedIndex, setOpenedIndex] = useState(null); + const [fields, setFields] = useState([]); + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }) + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + const oldIndex = fields.findIndex((field) => field.id === active.id); + const newIndex = fields.findIndex((field) => field.id === over.id); + + if (oldIndex !== -1 && newIndex !== -1) { + const newFields = arrayMove(fields, oldIndex, newIndex); + setFields(newFields); + } + } + }; + + const addNewField = () => { + setFields([ + ...fields, + { + id: uuidv4(), + description: "", + title: "", + type: "textinput", + }, + ]); + }; + + return ( + +
    +

    Основная информация

    + + Создайте поля для заполнения информации о приглашённых участниках и + задайте их размер + +
    + + + field.id)} + strategy={verticalListSortingStrategy} + > +
    +
    +

    Имя, фамилия

    +
    + {fields.map((field, index) => ( + setOpenedIndex(index)} + onClose={() => setOpenedIndex(null)} + handleDelete={() => { + const updated = fields.filter((_, i) => i !== index); + setFields(updated); + if (openedIndex === index) setOpenedIndex(null); + }} + /> + ))} +
    + Добавить поле +
    +
    +
    +
    + + navigate("/community/create/with_chat/connect_chat")} + /> +
    +
    + ); +}; + +export default CommunityWithChatProfilePage; diff --git a/client/src/pages/create_community/withChat/createCommunityWithChatLayout.tsx b/client/src/pages/create_community/withChat/createCommunityWithChatLayout.tsx index 2b27d6b..1b9171b 100644 --- a/client/src/pages/create_community/withChat/createCommunityWithChatLayout.tsx +++ b/client/src/pages/create_community/withChat/createCommunityWithChatLayout.tsx @@ -1,11 +1,55 @@ -import { Outlet } from "react-router-dom"; +import { AnimatePresence, motion } from "motion/react"; +import { Outlet, useLocation } from "react-router-dom"; const CreateCommunityWithChatLayout = () => { + const location = useLocation(); + + const steps = [ + "/community/create/with_chat/description", + "/community/create/with_chat/profile", + "/community/create/with_chat/connect_chat", + ]; + + const activeIndex = steps.findIndex((step) => + location.pathname.startsWith(step) + ); + return ( - <> - with chat +
    + + {!location.pathname.startsWith("/community/create/initial") && ( +
    +
    +
    + +
    +
    + + {steps.map((_, index) => ( +
    + {index < activeIndex && ( +
    + )} + {index >= activeIndex && ( + + )} +
    + ))} +
    + )} + - +
    ); }; diff --git a/client/src/pages/create_community/withoutChat/createCommunityWithoutChatLayout.tsx b/client/src/pages/create_community/withoutChat/createCommunityWithoutChatLayout.tsx index f84b39c..2a27e74 100644 --- a/client/src/pages/create_community/withoutChat/createCommunityWithoutChatLayout.tsx +++ b/client/src/pages/create_community/withoutChat/createCommunityWithoutChatLayout.tsx @@ -1,10 +1,75 @@ -import { Outlet } from "react-router-dom"; +import { AnimatePresence, motion } from "motion/react"; +import { useState, useEffect } from "react"; +import { Outlet, useLocation } from "react-router-dom"; const CreateCommunityWithoutChatLayout = () => { + const location = useLocation(); + + const steps = [ + "/community/create/without_chat/description", + "/community/create/without_chat/profile", + ]; + + const activeIndex = steps.findIndex((step) => + location.pathname.startsWith(step) + ); + + const [maxVisited, setMaxVisited] = useState(activeIndex); + + useEffect(() => { + setMaxVisited((prev) => Math.max(prev, activeIndex)); + }, [activeIndex]); + + const barTransition = { duration: 0.4, ease: "easeInOut" }; + return ( - <> - without chat - +
    + {!location.pathname.startsWith("/community/create/initial") && ( +
    +
    +
    + +
    +
    + {steps.map((_, index) => ( +
    + + {index < activeIndex && ( +
    + )} + + {index === activeIndex && ( + + )} + + {index > activeIndex && index > maxVisited && ( + + )} + +
    + ))} +
    + )} + + +
    ); }; diff --git a/client/src/stores/create_community/communityInfo.store.ts b/client/src/stores/create_community/communityInfo.store.ts new file mode 100644 index 0000000..a383087 --- /dev/null +++ b/client/src/stores/create_community/communityInfo.store.ts @@ -0,0 +1,38 @@ +import { create } from "zustand"; +import { Field } from "../../types/fields/field.interface"; + +interface CommunityInfoStore { + description: string; + fields: Field[]; + chatRequired: boolean; + chatId: string; + + setDescription: (v: string) => void; + setFields: (fields: Field[]) => void; + setChatRequired: (v: boolean) => void; + setChatId: (v: string) => void; + + resetStore: () => void; +} + +const useCommunityInfoStore = create((set) => ({ + description: "", + fields: [], + chatRequired: false, + chatId: "", + + setDescription: (v) => set({ description: v }), + setFields: (fields) => set({ fields }), + setChatRequired: (v) => set({ chatRequired: v }), + setChatId: (v) => set({ chatId: v }), + + resetStore: () => + set({ + description: "", + fields: [], + chatRequired: false, + chatId: "", + }), +})); + +export default useCommunityInfoStore; From c3241a0b44da9eff84ab9c65ae817d12146c4662 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 16 May 2025 15:24:30 +0300 Subject: [PATCH 35/78] mobile device drag-n-drop --- .../src/components/communityProfileField.tsx | 65 +++++++++-------- client/src/components/sortableItem.tsx | 31 +++++++++ .../withChat/communityWithChatProfilePage.tsx | 69 +++++++++++-------- 3 files changed, 105 insertions(+), 60 deletions(-) create mode 100644 client/src/components/sortableItem.tsx diff --git a/client/src/components/communityProfileField.tsx b/client/src/components/communityProfileField.tsx index 6e04d14..4b18e31 100644 --- a/client/src/components/communityProfileField.tsx +++ b/client/src/components/communityProfileField.tsx @@ -5,12 +5,8 @@ import TextinputIconDeactivate from "../assets/fieldIcons/textinput_deactivate.s import TextareaIconActivate from "../assets/fieldIcons/textarea_activate.svg"; import TextinputIconActivate from "../assets/fieldIcons/textinput_activate.svg"; import TextareaIconDeactivate from "../assets/fieldIcons/textarea_deactivate.svg"; -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; interface CommunityProfileFieldProps { - id: string; - index: number; handleDelete: () => void; isOpen: boolean; onOpen: () => void; @@ -20,44 +16,37 @@ interface CommunityProfileFieldProps { const CommunityProfileField = (props: CommunityProfileFieldProps) => { const [fieldType, setFieldType] = useState("textinput"); - const { attributes, listeners, setNodeRef, transform, transition } = - useSortable({ - id: props.id, - }); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - }; - const getFieldImage = () => { switch (fieldType) { case "textinput": return TextinputIconDeactivate; case "textarea": return TextareaIconDeactivate; - default: - return TextinputIconDeactivate; } }; + const preventTouch = (e: React.TouchEvent) => { + e.stopPropagation(); + }; + return ( -
    -
    - +
    +
    +
    { - e.stopPropagation(); + onClick={() => { props.onOpen(); }} + onTouchStart={preventTouch} > - Иконка поля + Field icon
    {props.isOpen && (
    @@ -69,9 +58,10 @@ const CommunityProfileField = (props: CommunityProfileFieldProps) => { setFieldType("textarea"); props.onClose(); }} + onTouchStart={preventTouch} > абзац - Активный абзац + Textarea active
    { setFieldType("textinput"); props.onClose(); }} + onTouchStart={preventTouch} > строка - Неактивная строка + Textinput deactivate
    )} @@ -93,9 +87,10 @@ const CommunityProfileField = (props: CommunityProfileFieldProps) => { setFieldType("textinput"); props.onClose(); }} + onTouchStart={preventTouch} > строка - Активная строка + Textinput active
    { setFieldType("textarea"); props.onClose(); }} + onTouchStart={preventTouch} > абзац - Неактивный абзац + Textarea deactivate
    )}
    )}
    -
    +
    Удалить
    diff --git a/client/src/components/sortableItem.tsx b/client/src/components/sortableItem.tsx new file mode 100644 index 0000000..674c23a --- /dev/null +++ b/client/src/components/sortableItem.tsx @@ -0,0 +1,31 @@ +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +interface SortableItemProps { + id: string; + children: React.ReactNode; +} + +export const SortableItem = ({ id, children }: SortableItemProps) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.7 : 1, + touchAction: "none", + }; + + return ( +
    + {children} +
    + ); +}; diff --git a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx index b312fd2..b3cee2b 100644 --- a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx @@ -1,6 +1,5 @@ import { useNavigate } from "react-router-dom"; import EBBComponent from "../../../components/enableBackButtonComponent"; -import FixedBottomButtonComponent from "../../../components/fixedBottomButton.component"; import Plus from "../../../assets/plus.svg"; import { useState } from "react"; import { Field } from "../../../types/fields/field.interface"; @@ -11,40 +10,54 @@ import { useSensor, useSensors, PointerSensor, - DragEndEvent, + TouchSensor, } from "@dnd-kit/core"; import { + arrayMove, SortableContext, verticalListSortingStrategy, - arrayMove, } from "@dnd-kit/sortable"; import { v4 as uuidv4 } from "uuid"; +import FixedBottomButtonComponent from "../../../components/fixedBottomButton.component"; +import { SortableItem } from "../../../components/sortableItem"; -export type FieldWithId = Field & { id: string }; +interface ExtendedField extends Field { + id: string; +} const CommunityWithChatProfilePage = () => { const navigate = useNavigate(); const [openedIndex, setOpenedIndex] = useState(null); - const [fields, setFields] = useState([]); + const [fields, setFields] = useState([]); + // Настройка сенсоров const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 150, + tolerance: 10, + }, }) ); - const handleDragEnd = (event: DragEndEvent) => { + // Обработка перетаскивания + const handleDragEnd = (event: any) => { const { active, over } = event; + console.log("Drag event:", { active, over }); // Для отладки - if (over && active.id !== over.id) { - const oldIndex = fields.findIndex((field) => field.id === active.id); - const newIndex = fields.findIndex((field) => field.id === over.id); - - if (oldIndex !== -1 && newIndex !== -1) { - const newFields = arrayMove(fields, oldIndex, newIndex); - setFields(newFields); + if (active.id !== over?.id) { + setFields((items) => { + const oldIndex = items.findIndex((item) => item.id === active.id); + const newIndex = items.findIndex((item) => item.id === over.id); + return arrayMove(items, oldIndex, newIndex); + }); + if (openedIndex !== null) { + setOpenedIndex(null); } } }; @@ -61,6 +74,13 @@ const CommunityWithChatProfilePage = () => { ]); }; + const handleDelete = (index: number) => { + const updated = [...fields]; + updated.splice(index, 1); + setFields(updated); + if (openedIndex === index) setOpenedIndex(null); + }; + return (
    @@ -85,25 +105,20 @@ const CommunityWithChatProfilePage = () => {

    Имя, фамилия

    {fields.map((field, index) => ( - setOpenedIndex(index)} - onClose={() => setOpenedIndex(null)} - handleDelete={() => { - const updated = fields.filter((_, i) => i !== index); - setFields(updated); - if (openedIndex === index) setOpenedIndex(null); - }} - /> + + setOpenedIndex(index)} + onClose={() => setOpenedIndex(null)} + handleDelete={() => handleDelete(index)} + /> + ))}
    - Добавить поле +
    From c7c577766b5cdda4d6b8eb319281fd36111262b9 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 16 May 2025 18:35:19 +0300 Subject: [PATCH 36/78] fix nav bug --- client/package-lock.json | 15 +++++++++++++++ client/package.json | 1 + client/src/App.tsx | 10 +++++++--- client/src/components/communityProfileField.tsx | 13 ++++++++----- client/src/main.tsx | 2 +- .../withChat/communityWithChatProfilePage.tsx | 5 +++-- client/src/pages/initial.page.tsx | 4 ++-- 7 files changed, 37 insertions(+), 13 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 15ffc4f..d41092c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@tailwindcss/vite": "^4.0.13", "@tanstack/react-query": "^5.74.8", @@ -364,6 +365,20 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@dnd-kit/modifiers": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", + "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, "node_modules/@dnd-kit/sortable": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", diff --git a/client/package.json b/client/package.json index bc14ac3..e223f47 100644 --- a/client/package.json +++ b/client/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@tailwindcss/vite": "^4.0.13", "@tanstack/react-query": "^5.74.8", diff --git a/client/src/App.tsx b/client/src/App.tsx index 21c6dc2..0b67302 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -43,10 +43,14 @@ function App() { ); const setBackButtonHandler = useCallback(() => { - backButton.onClick(() => navigate(-1)); - }, [navigate]); + backButton.onClick(() => { + navigate(-1); + }); - useEffect(setBackButtonHandler, [setBackButtonHandler]); + return backButton.offClick(() => {}); + }, []); + + useEffect(() => setBackButtonHandler, []); const routes = ( diff --git a/client/src/components/communityProfileField.tsx b/client/src/components/communityProfileField.tsx index 4b18e31..00ad1b7 100644 --- a/client/src/components/communityProfileField.tsx +++ b/client/src/components/communityProfileField.tsx @@ -36,17 +36,20 @@ const CommunityProfileField = (props: CommunityProfileFieldProps) => { style={{ touchAction: "none" }} >
    { - props.onOpen(); - }} + onClick={props.onOpen} onTouchStart={preventTouch} + className="flex items-center justify-center w-[32px] h-[32px] rounded-[8px] cursor-pointer" > - Field icon + Тип поля
    {props.isOpen && (
    diff --git a/client/src/main.tsx b/client/src/main.tsx index d3e8bd7..0d8c9af 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -14,5 +14,5 @@ createRoot(document.getElementById("root")!).render( - , + ); diff --git a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx index b3cee2b..67dfd3d 100644 --- a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx @@ -4,6 +4,7 @@ import Plus from "../../../assets/plus.svg"; import { useState } from "react"; import { Field } from "../../../types/fields/field.interface"; import CommunityProfileField from "../../../components/communityProfileField"; +import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; import { DndContext, closestCenter, @@ -45,10 +46,9 @@ const CommunityWithChatProfilePage = () => { }) ); - // Обработка перетаскивания const handleDragEnd = (event: any) => { const { active, over } = event; - console.log("Drag event:", { active, over }); // Для отладки + console.log("Drag event:", { active, over }); if (active.id !== over?.id) { setFields((items) => { @@ -95,6 +95,7 @@ const CommunityWithChatProfilePage = () => { sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} + modifiers={[restrictToVerticalAxis]} // Ограничиваем движение по оси Y > field.id)} diff --git a/client/src/pages/initial.page.tsx b/client/src/pages/initial.page.tsx index 8edf9cf..53fb004 100644 --- a/client/src/pages/initial.page.tsx +++ b/client/src/pages/initial.page.tsx @@ -12,10 +12,10 @@ const InitialPage = () => { startParam !== null && startParam.length > 0 ) { - navigate("/communities", { replace: true }); + navigate("/communities"); navigate(`/community/${startParam}`); } else { - navigate("/communities", { replace: true }); + navigate("/communities"); } }, []); From 11b2aae36d23e3b8ba9337c5f1c3db69a81b4dad Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 16 May 2025 18:53:15 +0300 Subject: [PATCH 37/78] finalyy done freaking fields constructor --- client/src/components/communityProfileField.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/client/src/components/communityProfileField.tsx b/client/src/components/communityProfileField.tsx index 00ad1b7..0aeb048 100644 --- a/client/src/components/communityProfileField.tsx +++ b/client/src/components/communityProfileField.tsx @@ -32,25 +32,20 @@ const CommunityProfileField = (props: CommunityProfileFieldProps) => { return (
    -
    - Тип поля -
    + /> {props.isOpen && (
    {fieldType === "textarea" && ( From 152c4be033b697d3bf80d552388750c3fd7665a4 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 16 May 2025 19:07:00 +0300 Subject: [PATCH 38/78] store memorizing fields --- .../src/components/communityProfileField.tsx | 4 +++ client/src/mappers/fieldsToFieldsWithId.ts | 19 ++++++++++++ .../withChat/communityWithChatProfilePage.tsx | 31 +++++++++++++++---- 3 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 client/src/mappers/fieldsToFieldsWithId.ts diff --git a/client/src/components/communityProfileField.tsx b/client/src/components/communityProfileField.tsx index 0aeb048..b72b3dc 100644 --- a/client/src/components/communityProfileField.tsx +++ b/client/src/components/communityProfileField.tsx @@ -11,6 +11,8 @@ interface CommunityProfileFieldProps { isOpen: boolean; onOpen: () => void; onClose: () => void; + value: string; + onChange: (v: string) => void; } const CommunityProfileField = (props: CommunityProfileFieldProps) => { @@ -36,6 +38,8 @@ const CommunityProfileField = (props: CommunityProfileFieldProps) => { style={{ touchAction: "none" }} > props.onChange(e.target.value)} className="outline-none w-[80%]" placeholder="Название поля" onTouchStart={preventTouch} diff --git a/client/src/mappers/fieldsToFieldsWithId.ts b/client/src/mappers/fieldsToFieldsWithId.ts new file mode 100644 index 0000000..c668dd0 --- /dev/null +++ b/client/src/mappers/fieldsToFieldsWithId.ts @@ -0,0 +1,19 @@ +import { ExtendedField } from "../pages/create_community/withChat/communityWithChatProfilePage"; +import { Field } from "../types/fields/field.interface"; +import { v4 as uuidv4 } from "uuid"; + +export const fieldsToFieldsWithId = (fields: Field[]): ExtendedField[] => { + return fields.map((field) => ({ ...field, id: uuidv4() })); +}; + +export const fieldsWithIdToFields = ( + extendedFilds: ExtendedField[] +): Field[] => { + return extendedFilds.map((efield) => ({ + description: efield.description, + textarea: efield.textarea, + textinput: efield.textinput, + type: efield.type, + title: efield.title, + })); +}; diff --git a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx index 67dfd3d..37e2737 100644 --- a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx @@ -21,17 +21,29 @@ import { import { v4 as uuidv4 } from "uuid"; import FixedBottomButtonComponent from "../../../components/fixedBottomButton.component"; import { SortableItem } from "../../../components/sortableItem"; +import useCommunityInfoStore from "../../../stores/create_community/communityInfo.store"; +import { + fieldsToFieldsWithId, + fieldsWithIdToFields, +} from "../../../mappers/fieldsToFieldsWithId"; -interface ExtendedField extends Field { +export interface ExtendedField extends Field { id: string; } - const CommunityWithChatProfilePage = () => { + const { fields: storeFields, setFields: setStoreFields } = + useCommunityInfoStore(); const navigate = useNavigate(); const [openedIndex, setOpenedIndex] = useState(null); - const [fields, setFields] = useState([]); + const [fields, setFields] = useState( + fieldsToFieldsWithId(storeFields) + ); + + const handleContinue = () => { + setStoreFields(fieldsWithIdToFields(fields)); + navigate("/community/create/with_chat/connect_chat"); + }; - // Настройка сенсоров const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { @@ -95,7 +107,7 @@ const CommunityWithChatProfilePage = () => { sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} - modifiers={[restrictToVerticalAxis]} // Ограничиваем движение по оси Y + modifiers={[restrictToVerticalAxis]} > field.id)} @@ -108,6 +120,12 @@ const CommunityWithChatProfilePage = () => { {fields.map((field, index) => ( { + const updated = [...fields]; + updated[index] = { ...updated[index], title: newValue }; + setFields(updated); + }} isOpen={openedIndex === index} onOpen={() => setOpenedIndex(index)} onClose={() => setOpenedIndex(null)} @@ -115,6 +133,7 @@ const CommunityWithChatProfilePage = () => { /> ))} +
    { navigate("/community/create/with_chat/connect_chat")} + handleClick={() => handleContinue()} />
    From 7d10ff710895c0551fdb0ec338e020346a8b761d Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 16 May 2025 19:28:15 +0300 Subject: [PATCH 39/78] fix memorizing field type --- client/src/components/communityProfileField.tsx | 10 +++++++++- .../withChat/communityWithChatProfilePage.tsx | 7 +++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/client/src/components/communityProfileField.tsx b/client/src/components/communityProfileField.tsx index b72b3dc..caf0ed6 100644 --- a/client/src/components/communityProfileField.tsx +++ b/client/src/components/communityProfileField.tsx @@ -13,10 +13,12 @@ interface CommunityProfileFieldProps { onClose: () => void; value: string; onChange: (v: string) => void; + type: FieldType; + onTypeChange: (type: FieldType) => void; } const CommunityProfileField = (props: CommunityProfileFieldProps) => { - const [fieldType, setFieldType] = useState("textinput"); + const [fieldType, setFieldType] = useState(props.type); const getFieldImage = () => { switch (fieldType) { @@ -58,6 +60,7 @@ const CommunityProfileField = (props: CommunityProfileFieldProps) => { className="flex flex-row items-center justify-end gap-[12px] text-black" onClick={() => { setFieldType("textarea"); + props.onTypeChange("textarea"); props.onClose(); }} onTouchStart={preventTouch} @@ -69,6 +72,8 @@ const CommunityProfileField = (props: CommunityProfileFieldProps) => { className="flex flex-row gap-[12px] items-center" onClick={() => { setFieldType("textinput"); + + props.onTypeChange("textinput"); props.onClose(); }} onTouchStart={preventTouch} @@ -87,6 +92,8 @@ const CommunityProfileField = (props: CommunityProfileFieldProps) => { className="flex flex-row gap-[12px] text-black items-center justify-end" onClick={() => { setFieldType("textinput"); + + props.onTypeChange("textinput"); props.onClose(); }} onTouchStart={preventTouch} @@ -98,6 +105,7 @@ const CommunityProfileField = (props: CommunityProfileFieldProps) => { className="flex flex-row gap-[12px] items-center justify-end" onClick={() => { setFieldType("textarea"); + props.onTypeChange("textarea"); props.onClose(); }} onTouchStart={preventTouch} diff --git a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx index 37e2737..c204488 100644 --- a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx @@ -26,6 +26,7 @@ import { fieldsToFieldsWithId, fieldsWithIdToFields, } from "../../../mappers/fieldsToFieldsWithId"; +import { FieldType } from "../../../types/fields/field.type"; export interface ExtendedField extends Field { id: string; @@ -126,10 +127,16 @@ const CommunityWithChatProfilePage = () => { updated[index] = { ...updated[index], title: newValue }; setFields(updated); }} + type={field.type} isOpen={openedIndex === index} onOpen={() => setOpenedIndex(index)} onClose={() => setOpenedIndex(null)} handleDelete={() => handleDelete(index)} + onTypeChange={(type: FieldType) => { + const updated = [...fields]; + updated[index] = { ...updated[index], type: type }; + setFields(updated); + }} /> ))} From 0becb6b65206d352f4422b8f5691054806ba8d83 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 16 May 2025 22:55:56 +0300 Subject: [PATCH 40/78] lint fix --- client/src/assets/copy_icon.svg | 3 + client/src/assets/plus_white.svg | 3 + client/src/components/step.component.tsx | 16 ++++ .../withChat/communityWithChatConnectPage.tsx | 77 ++++++++++++++++++- .../communityWithChatDescriptionPage.tsx | 1 - 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 client/src/assets/copy_icon.svg create mode 100644 client/src/assets/plus_white.svg create mode 100644 client/src/components/step.component.tsx diff --git a/client/src/assets/copy_icon.svg b/client/src/assets/copy_icon.svg new file mode 100644 index 0000000..666b3c6 --- /dev/null +++ b/client/src/assets/copy_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/plus_white.svg b/client/src/assets/plus_white.svg new file mode 100644 index 0000000..8e2d460 --- /dev/null +++ b/client/src/assets/plus_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/components/step.component.tsx b/client/src/components/step.component.tsx new file mode 100644 index 0000000..6219d40 --- /dev/null +++ b/client/src/components/step.component.tsx @@ -0,0 +1,16 @@ +export interface StepProps { + stepNumber: number; + value: string; +} +const StepComponent = ({ stepNumber, value }: StepProps) => { + return ( +
    +
    + {stepNumber} +
    +
    {value}
    +
    + ); +}; + +export default StepComponent; diff --git a/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx b/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx index 8cf470d..2bd871a 100644 --- a/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx @@ -1,5 +1,80 @@ +import EBBComponent from "../../../components/enableBackButtonComponent"; +import StepComponent from "../../../components/step.component"; +import Plus from "../../../assets/plus_white.svg"; +import CopyIcon from "../../../assets/copy_icon.svg"; +import { openTelegramLink } from "@telegram-apps/sdk-react"; +import { BOT_USERNAME } from "../../../shared/constants"; +import { useState } from "react"; +import FixedBottomButtonComponent from "../../../components/fixedBottomButton.component"; const CommunityWithChatConnectPage = () => { - return <>Connect Page; + const [command] = useState("/create SVO4Z"); + + return ( + +
    +

    Привязка к чату

    +
    + +
    +
    + +
    +

    {command}

    + { + navigator.clipboard.writeText(command); + }} + /> +
    +
    + +
    + +
    { + openTelegramLink(`https://t.me/${BOT_USERNAME}?startgroup=`); + }} + > +

    Добавить в чат

    + +
    +
    + +
    + +
    +
    + 1 +
    +
    + * привязать сообщество к чату +
    можно только один раз +
    +
    +
    +
    + + {}} + /> +
    +
    + ); }; export default CommunityWithChatConnectPage; diff --git a/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx b/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx index fee04d6..fba2902 100644 --- a/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx @@ -1,5 +1,4 @@ import { useNavigate } from "react-router-dom"; -import ButtonComponent from "../../../components/button.component"; import EBBComponent from "../../../components/enableBackButtonComponent"; import TextareaFieldComponent from "../../../components/form/textareaField.component"; import useCommunityInfoStore from "../../../stores/create_community/communityInfo.store"; From 54a9b31ba7022442f36c85f9448f26d83573fdff Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 16 May 2025 23:05:53 +0300 Subject: [PATCH 41/78] minor fix --- client/src/App.tsx | 2 +- .../create_community/withChat/communityWithChatProfilePage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 0b67302..04a8593 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -50,7 +50,7 @@ function App() { return backButton.offClick(() => {}); }, []); - useEffect(() => setBackButtonHandler, []); + useEffect(() => setBackButtonHandler, [setBackButtonHandler]); const routes = ( diff --git a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx index c204488..6f8f4fb 100644 --- a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx @@ -97,7 +97,7 @@ const CommunityWithChatProfilePage = () => { return (
    -

    Основная информация

    +

    Профиль участника

    Создайте поля для заполнения информации о приглашённых участниках и задайте их размер From 683e1b900890f08eaa9a2b9eee4f42c2957b8478 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Fri, 16 May 2025 23:11:26 +0300 Subject: [PATCH 42/78] back button fix --- client/src/App.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 04a8593..19d4064 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -46,11 +46,9 @@ function App() { backButton.onClick(() => { navigate(-1); }); - - return backButton.offClick(() => {}); }, []); - useEffect(() => setBackButtonHandler, [setBackButtonHandler]); + useEffect(() => setBackButtonHandler(), [setBackButtonHandler]); const routes = ( From a719bce51431a31b0bbfd3d02a6cdbceb3d5b1b7 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Sat, 17 May 2025 11:57:30 +0300 Subject: [PATCH 43/78] community creation --- client/public/config.js | 2 +- client/src/App.tsx | 6 + client/src/assets/star_buka.png | Bin 0 -> 10098 bytes client/src/assets/upload.svg | 3 + .../mutations/useCreateCommunity.ts | 25 +++ client/src/pages/communityLinks.page.tsx | 4 + client/src/pages/communityQR.page.tsx | 0 .../withChat/communityWithChatConnectPage.tsx | 73 ++++++-- .../communityWithChatDescriptionPage.tsx | 4 +- .../withChat/communityWithChatProfilePage.tsx | 25 ++- .../communityWithoutChatDescriptionPage.tsx | 127 +++++++++++++- .../communityWithoutChatProfilePage.tsx | 164 ++++++++++++++++++ .../createCommunityWithoutChatLayout.tsx | 73 +++----- client/src/shared/constants.ts | 2 + ...tore.ts => communityWithChatInfo.store.ts} | 4 +- .../communityWithoutChatInfo.store.ts | 46 +++++ client/src/types/fields/field.type.ts | 1 + 17 files changed, 488 insertions(+), 71 deletions(-) create mode 100644 client/src/assets/star_buka.png create mode 100644 client/src/assets/upload.svg create mode 100644 client/src/hooks/communities/mutations/useCreateCommunity.ts create mode 100644 client/src/pages/communityLinks.page.tsx create mode 100644 client/src/pages/communityQR.page.tsx create mode 100644 client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx rename client/src/stores/create_community/{communityInfo.store.ts => communityWithChatInfo.store.ts} (86%) create mode 100644 client/src/stores/create_community/communityWithoutChatInfo.store.ts diff --git a/client/public/config.js b/client/public/config.js index 0832de4..b44b81a 100644 --- a/client/public/config.js +++ b/client/public/config.js @@ -1,4 +1,4 @@ window.api = { - BOT_USERNAME: "vaniog_tglink_bot", + BOT_USERNAME: "leenky_dev_bot", API_URL: "https://app.dev.leenky.ru", }; diff --git a/client/src/App.tsx b/client/src/App.tsx index 19d4064..3148626 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -24,6 +24,7 @@ import CommunityWithoutChatDescriptionPage from "./pages/create_community/withou import CommunityWithChatProfilePage from "./pages/create_community/withChat/communityWithChatProfilePage"; import CommunityWithChatDescriptionPage from "./pages/create_community/withChat/communityWithChatDescriptionPage"; import CommunityWithChatConnectPage from "./pages/create_community/withChat/communityWithChatConnectPage"; +import CommunityWithoutChatProfilePage from "./pages/create_community/withoutChat/communityWithoutChatProfilePage"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -77,6 +78,11 @@ function App() { path="description" element={} /> + + } + /> diff --git a/client/src/assets/star_buka.png b/client/src/assets/star_buka.png new file mode 100644 index 0000000000000000000000000000000000000000..260bdb04e328a0d0a562c4962db2c3779070d7a6 GIT binary patch literal 10098 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 Ua8MW4`2YX_07*qoM6N<$f}9Ue=>Px# literal 0 HcmV?d00001 diff --git a/client/src/assets/upload.svg b/client/src/assets/upload.svg new file mode 100644 index 0000000..8768d0c --- /dev/null +++ b/client/src/assets/upload.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/hooks/communities/mutations/useCreateCommunity.ts b/client/src/hooks/communities/mutations/useCreateCommunity.ts new file mode 100644 index 0000000..c5e847c --- /dev/null +++ b/client/src/hooks/communities/mutations/useCreateCommunity.ts @@ -0,0 +1,25 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import useInitDataStore from "../../../stores/InitData.store"; +import { createCommunity, joinCommunity } from "../../../api/communities.api"; +import { CreateCommunity } from "../../../types/postApiTypes/createCommunity.interface"; + +const useCreateCommunity = () => { + const { initData } = useInitDataStore(); + + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ communityData }: { communityData: CreateCommunity }) => + createCommunity(initData, communityData), + onSuccess: async (_, communityId) => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["/communities", initData] }), + queryClient.refetchQueries({ + queryKey: [`/communities/${communityId}`, initData], + }), + ]); + }, + }); +}; + +export default useCreateCommunity; diff --git a/client/src/pages/communityLinks.page.tsx b/client/src/pages/communityLinks.page.tsx new file mode 100644 index 0000000..b4471dc --- /dev/null +++ b/client/src/pages/communityLinks.page.tsx @@ -0,0 +1,4 @@ +import StarsBuka from "../assets/star_buka.png"; +const CommunityLinksPage = () => { + return; +}; diff --git a/client/src/pages/communityQR.page.tsx b/client/src/pages/communityQR.page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx b/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx index 2bd871a..81feefd 100644 --- a/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx @@ -4,10 +4,51 @@ import Plus from "../../../assets/plus_white.svg"; import CopyIcon from "../../../assets/copy_icon.svg"; import { openTelegramLink } from "@telegram-apps/sdk-react"; import { BOT_USERNAME } from "../../../shared/constants"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import FixedBottomButtonComponent from "../../../components/fixedBottomButton.component"; +import useCreateCommunity from "../../../hooks/communities/mutations/useCreateCommunity"; +import useCommunityWithChatInfoStore from "../../../stores/create_community/communityWithChatInfo.store"; +import { useNavigate } from "react-router-dom"; const CommunityWithChatConnectPage = () => { - const [command] = useState("/create SVO4Z"); + const { fields, setChatId, chatId, description } = + useCommunityWithChatInfoStore(); + const [command] = useState(`/connect ${chatId}`); + const createCommunityMutation = useCreateCommunity(); + const [isLoading, setIsLoading] = useState(false); + const navigate = useNavigate(); + useEffect(() => { + const createCommunity = async () => { + try { + setIsLoading(true); + console.log(fields); + const community = await createCommunityMutation.mutateAsync({ + communityData: { + avatar: "", + config: { + fields: fields, + }, + description: description, + name: "", + }, + }); + + if (community) { + setChatId(community.id); + } else { + alert("Уупс, произошла ошибка"); + navigate(-1); + } + } catch (error) { + console.error("Ошибка создания сообщества:", error); + alert("Произошла ошибка при создании сообщества"); + navigate(-1); + } finally { + setIsLoading(false); + } + }; + + createCommunity(); + }, []); return ( @@ -21,17 +62,23 @@ const CommunityWithChatConnectPage = () => { stepNumber={1} value="Скопируйте команду с вашим ChatID" /> -
    -

    {command}

    - { - navigator.clipboard.writeText(command); - }} - /> -
    + {isLoading ? ( +
    +
    +
    + ) : ( +
    +

    {command}

    + { + navigator.clipboard.writeText(command); + }} + /> +
    + )}
    diff --git a/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx b/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx index fba2902..91d25ca 100644 --- a/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatDescriptionPage.tsx @@ -1,13 +1,13 @@ import { useNavigate } from "react-router-dom"; import EBBComponent from "../../../components/enableBackButtonComponent"; import TextareaFieldComponent from "../../../components/form/textareaField.component"; -import useCommunityInfoStore from "../../../stores/create_community/communityInfo.store"; +import useCommunityWithChatInfoStore from "../../../stores/create_community/communityWithChatInfo.store"; import FixedBottomButtonComponent from "../../../components/fixedBottomButton.component"; const CommunityWithChatDescriptionPage = () => { const navigate = useNavigate(); - const { setDescription, description } = useCommunityInfoStore(); + const { setDescription, description } = useCommunityWithChatInfoStore(); return (
    diff --git a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx index 6f8f4fb..32e63c2 100644 --- a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx @@ -21,7 +21,7 @@ import { import { v4 as uuidv4 } from "uuid"; import FixedBottomButtonComponent from "../../../components/fixedBottomButton.component"; import { SortableItem } from "../../../components/sortableItem"; -import useCommunityInfoStore from "../../../stores/create_community/communityInfo.store"; +import useCommunityWithChatInfoStore from "../../../stores/create_community/communityWithChatInfo.store"; import { fieldsToFieldsWithId, fieldsWithIdToFields, @@ -33,7 +33,8 @@ export interface ExtendedField extends Field { } const CommunityWithChatProfilePage = () => { const { fields: storeFields, setFields: setStoreFields } = - useCommunityInfoStore(); + useCommunityWithChatInfoStore(); + console.log(storeFields); const navigate = useNavigate(); const [openedIndex, setOpenedIndex] = useState(null); const [fields, setFields] = useState( @@ -83,8 +84,13 @@ const CommunityWithChatProfilePage = () => { description: "", title: "", type: "textinput", + textinput: { + default: "", + }, }, ]); + + console.log(storeFields); }; const handleDelete = (index: number) => { @@ -134,8 +140,17 @@ const CommunityWithChatProfilePage = () => { handleDelete={() => handleDelete(index)} onTypeChange={(type: FieldType) => { const updated = [...fields]; - updated[index] = { ...updated[index], type: type }; - setFields(updated); + console.log(updated[index]); + updated[index] = { + ...updated[index], + type: type, + [type]: { default: "" }, + }; + + console.log(updated[index]); + console.log(updated); + setFields([...updated]); + console.log(fields); }} /> @@ -156,7 +171,7 @@ const CommunityWithChatProfilePage = () => { state={true ? "active" : "disabled"} handleClick={() => handleContinue()} /> -
    +
    ); }; diff --git a/client/src/pages/create_community/withoutChat/communityWithoutChatDescriptionPage.tsx b/client/src/pages/create_community/withoutChat/communityWithoutChatDescriptionPage.tsx index d8f2ff5..449df9b 100644 --- a/client/src/pages/create_community/withoutChat/communityWithoutChatDescriptionPage.tsx +++ b/client/src/pages/create_community/withoutChat/communityWithoutChatDescriptionPage.tsx @@ -1,7 +1,132 @@ +import { useNavigate } from "react-router-dom"; import EBBComponent from "../../../components/enableBackButtonComponent"; +import TextareaFieldComponent from "../../../components/form/textareaField.component"; +import FixedBottomButtonComponent from "../../../components/fixedBottomButton.component"; +import InputFieldComponent from "../../../components/form/inputField.component"; +import useCommunityWithoutChatInfoStore from "../../../stores/create_community/communityWithoutChatInfo.store"; +import UploadImage from "../../../assets/upload.svg"; +import { useRef, useState } from "react"; +import { ALLOWED_IMAGE_TYPES, MAX_FILE_SIZE } from "../../../shared/constants"; const CommunityWithoutChatDescriptionPage = () => { - return Community Description; + const navigate = useNavigate(); + + const { setDescription, description, setName, name, avatar, setAvatar } = + useCommunityWithoutChatInfoStore(); + + const [previewUrl, setPreviewUrl] = useState( + avatar ? URL.createObjectURL(avatar) : "" + ); + + const [errorMessage, setErrorMessage] = useState(null); + + const inputRef = useRef(null); + + const handleUploadClick = () => { + setErrorMessage(null); + if (inputRef.current) { + inputRef.current.value = ""; + inputRef.current.click(); + } + }; + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + if (!ALLOWED_IMAGE_TYPES.includes(file.type)) { + setErrorMessage("Допустимы PNG или JPG"); + return; + } + + if (file.size > MAX_FILE_SIZE) { + setErrorMessage("Размер файла не должен превышать 5 МБ"); + return; + } + setAvatar(file); + setPreviewUrl(URL.createObjectURL(file)); + } + }; + + return ( + +
    +
    +

    + Основная информация +

    +
    + + + + + +
    +

    + Аватар сообщества +

    + +
    +
    + {avatar !== null && previewUrl !== null && ( +
    + avatar +
    + )} + +
    + Upload icon +

    загрузить с устройства

    +
    + +
    + {errorMessage && ( +
    + {errorMessage} +
    + )} +
    +
    +
    + +
    + { + if (name && description && avatar) + navigate("/community/create/without_chat/profile"); + }} + /> +
    +
    +
    + ); }; export default CommunityWithoutChatDescriptionPage; diff --git a/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx b/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx new file mode 100644 index 0000000..8755503 --- /dev/null +++ b/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx @@ -0,0 +1,164 @@ +import { useNavigate } from "react-router-dom"; +import EBBComponent from "../../../components/enableBackButtonComponent"; +import Plus from "../../../assets/plus.svg"; +import { useState } from "react"; +import { Field } from "../../../types/fields/field.interface"; +import CommunityProfileField from "../../../components/communityProfileField"; +import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; +import { + DndContext, + closestCenter, + useSensor, + useSensors, + PointerSensor, + TouchSensor, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { v4 as uuidv4 } from "uuid"; +import FixedBottomButtonComponent from "../../../components/fixedBottomButton.component"; +import { SortableItem } from "../../../components/sortableItem"; +import { + fieldsToFieldsWithId, + fieldsWithIdToFields, +} from "../../../mappers/fieldsToFieldsWithId"; +import { FieldType } from "../../../types/fields/field.type"; +import useCommunityWithoutChatInfoStore from "../../../stores/create_community/communityWithoutChatInfo.store"; + +export interface ExtendedField extends Field { + id: string; +} +const CommunityWithoutChatProfilePage = () => { + const { fields: storeFields, setFields: setStoreFields } = + useCommunityWithoutChatInfoStore(); + const navigate = useNavigate(); + const [openedIndex, setOpenedIndex] = useState(null); + const [fields, setFields] = useState( + fieldsToFieldsWithId(storeFields) + ); + + const handleContinue = () => { + setStoreFields(fieldsWithIdToFields(fields)); + navigate("/community/create/with_chat/connect_chat"); + }; + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 150, + tolerance: 10, + }, + }) + ); + + const handleDragEnd = (event: any) => { + const { active, over } = event; + console.log("Drag event:", { active, over }); + + if (active.id !== over?.id) { + setFields((items) => { + const oldIndex = items.findIndex((item) => item.id === active.id); + const newIndex = items.findIndex((item) => item.id === over.id); + return arrayMove(items, oldIndex, newIndex); + }); + if (openedIndex !== null) { + setOpenedIndex(null); + } + } + }; + + const addNewField = () => { + setFields([ + ...fields, + { + id: uuidv4(), + description: "", + title: "", + type: "textinput", + }, + ]); + }; + + const handleDelete = (index: number) => { + const updated = [...fields]; + updated.splice(index, 1); + setFields(updated); + if (openedIndex === index) setOpenedIndex(null); + }; + + return ( + +
    +

    Профиль участника

    + + Создайте поля для заполнения информации о приглашённых участниках и + задайте их размер + +
    + + + field.id)} + strategy={verticalListSortingStrategy} + > +
    +
    +

    Имя, фамилия

    +
    + {fields.map((field, index) => ( + + { + const updated = [...fields]; + updated[index] = { ...updated[index], title: newValue }; + setFields(updated); + }} + type={field.type} + isOpen={openedIndex === index} + onOpen={() => setOpenedIndex(index)} + onClose={() => setOpenedIndex(null)} + handleDelete={() => handleDelete(index)} + onTypeChange={(type: FieldType) => { + const updated = [...fields]; + updated[index] = { ...updated[index], type: type }; + setFields(updated); + }} + /> + + ))} + +
    + +
    +
    +
    +
    + + handleContinue()} + /> +
    +
    + ); +}; + +export default CommunityWithoutChatProfilePage; diff --git a/client/src/pages/create_community/withoutChat/createCommunityWithoutChatLayout.tsx b/client/src/pages/create_community/withoutChat/createCommunityWithoutChatLayout.tsx index 2a27e74..bcb54e7 100644 --- a/client/src/pages/create_community/withoutChat/createCommunityWithoutChatLayout.tsx +++ b/client/src/pages/create_community/withoutChat/createCommunityWithoutChatLayout.tsx @@ -1,5 +1,4 @@ import { AnimatePresence, motion } from "motion/react"; -import { useState, useEffect } from "react"; import { Outlet, useLocation } from "react-router-dom"; const CreateCommunityWithoutChatLayout = () => { @@ -14,60 +13,40 @@ const CreateCommunityWithoutChatLayout = () => { location.pathname.startsWith(step) ); - const [maxVisited, setMaxVisited] = useState(activeIndex); - - useEffect(() => { - setMaxVisited((prev) => Math.max(prev, activeIndex)); - }, [activeIndex]); - - const barTransition = { duration: 0.4, ease: "easeInOut" }; - return ( -
    - {!location.pathname.startsWith("/community/create/initial") && ( -
    -
    -
    +
    + + {!location.pathname.startsWith("/community/create/initial") && ( +
    +
    +
    + +
    +
    -
    -
    - {steps.map((_, index) => ( -
    - + {steps.map((_, index) => ( +
    {index < activeIndex && ( -
    +
    )} - - {index === activeIndex && ( + {index >= activeIndex && ( )} - - {index > activeIndex && index > maxVisited && ( - - )} - -
    - ))} -
    - )} - +
    + ))} +
    + )} +
    ); diff --git a/client/src/shared/constants.ts b/client/src/shared/constants.ts index 97d53c6..17fc4bf 100644 --- a/client/src/shared/constants.ts +++ b/client/src/shared/constants.ts @@ -1,2 +1,4 @@ export const API_URL = window.api.API_URL; export const BOT_USERNAME = window.api.BOT_USERNAME; +export const MAX_FILE_SIZE = 5 * 1024; //bytes +export const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg"]; diff --git a/client/src/stores/create_community/communityInfo.store.ts b/client/src/stores/create_community/communityWithChatInfo.store.ts similarity index 86% rename from client/src/stores/create_community/communityInfo.store.ts rename to client/src/stores/create_community/communityWithChatInfo.store.ts index a383087..9019ef9 100644 --- a/client/src/stores/create_community/communityInfo.store.ts +++ b/client/src/stores/create_community/communityWithChatInfo.store.ts @@ -15,7 +15,7 @@ interface CommunityInfoStore { resetStore: () => void; } -const useCommunityInfoStore = create((set) => ({ +const useCommunityWithChatInfoStore = create((set) => ({ description: "", fields: [], chatRequired: false, @@ -35,4 +35,4 @@ const useCommunityInfoStore = create((set) => ({ }), })); -export default useCommunityInfoStore; +export default useCommunityWithChatInfoStore; diff --git a/client/src/stores/create_community/communityWithoutChatInfo.store.ts b/client/src/stores/create_community/communityWithoutChatInfo.store.ts new file mode 100644 index 0000000..3d51917 --- /dev/null +++ b/client/src/stores/create_community/communityWithoutChatInfo.store.ts @@ -0,0 +1,46 @@ +import { create } from "zustand"; +import { Field } from "../../types/fields/field.interface"; + +interface CommunityInfoStore { + description: string; + fields: Field[]; + chatRequired: boolean; + chatId: string; + name: string; + avatar: File | null; + + setDescription: (v: string) => void; + setFields: (fields: Field[]) => void; + setChatRequired: (v: boolean) => void; + setChatId: (v: string) => void; + setName: (name: string) => void; + setAvatar: (img: File) => void; + resetStore: () => void; +} + +const useCommunityWithoutChatInfoStore = create((set) => ({ + description: "", + fields: [], + chatRequired: false, + name: "", + chatId: "", + avatar: null, + + setDescription: (v) => set({ description: v }), + setFields: (fields) => set({ fields }), + setChatRequired: (v) => set({ chatRequired: v }), + setChatId: (v) => set({ chatId: v }), + setName: (name: string) => set({ name: name }), + setAvatar: (img: File) => set({ avatar: img }), + resetStore: () => + set({ + description: "", + fields: [], + chatRequired: false, + chatId: "", + name: "", + avatar: null, + }), +})); + +export default useCommunityWithoutChatInfoStore; diff --git a/client/src/types/fields/field.type.ts b/client/src/types/fields/field.type.ts index 282427d..2805258 100644 --- a/client/src/types/fields/field.type.ts +++ b/client/src/types/fields/field.type.ts @@ -1 +1,2 @@ export type FieldType = "textinput" | "textarea"; +export const AllFieldTypes: FieldType[] = ["textinput", "textarea"]; From 985df017c70fc6df9b789d387e7139c41e628a59 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Sat, 17 May 2025 12:23:30 +0300 Subject: [PATCH 44/78] fix lint --- .../src/hooks/communities/mutations/useCreateCommunity.ts | 2 +- client/src/pages/communityLinks.page.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/src/hooks/communities/mutations/useCreateCommunity.ts b/client/src/hooks/communities/mutations/useCreateCommunity.ts index c5e847c..6da0324 100644 --- a/client/src/hooks/communities/mutations/useCreateCommunity.ts +++ b/client/src/hooks/communities/mutations/useCreateCommunity.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import useInitDataStore from "../../../stores/InitData.store"; -import { createCommunity, joinCommunity } from "../../../api/communities.api"; +import { createCommunity } from "../../../api/communities.api"; import { CreateCommunity } from "../../../types/postApiTypes/createCommunity.interface"; const useCreateCommunity = () => { diff --git a/client/src/pages/communityLinks.page.tsx b/client/src/pages/communityLinks.page.tsx index b4471dc..b3f07d4 100644 --- a/client/src/pages/communityLinks.page.tsx +++ b/client/src/pages/communityLinks.page.tsx @@ -1,4 +1,4 @@ -import StarsBuka from "../assets/star_buka.png"; -const CommunityLinksPage = () => { - return; -}; +// import StarsBuka from "../assets/star_buka.png"; +// const CommunityLinksPage = () => { +// return; +// }; From 31f458e38eeed4f214eb54ad167d453c17202b2d Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Sat, 17 May 2025 21:51:32 +0300 Subject: [PATCH 45/78] create community hook and patch admin connection --- .../mutations/useCreateCommunity.ts | 22 +++++- .../withChat/communityWithChatConnectPage.tsx | 71 ++++--------------- .../withChat/communityWithChatProfilePage.tsx | 50 +++++++++++-- .../communityWithChatInfo.store.ts | 9 +-- 4 files changed, 82 insertions(+), 70 deletions(-) diff --git a/client/src/hooks/communities/mutations/useCreateCommunity.ts b/client/src/hooks/communities/mutations/useCreateCommunity.ts index 6da0324..e0621f9 100644 --- a/client/src/hooks/communities/mutations/useCreateCommunity.ts +++ b/client/src/hooks/communities/mutations/useCreateCommunity.ts @@ -2,20 +2,36 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import useInitDataStore from "../../../stores/InitData.store"; import { createCommunity } from "../../../api/communities.api"; import { CreateCommunity } from "../../../types/postApiTypes/createCommunity.interface"; +import useUserStore from "../../../stores/user.store"; +import { fieldsToFieldValues } from "../../../mappers/FieldValues"; +import usePatchMember from "../../members/mutations/usePatchMember"; const useCreateCommunity = () => { + const patchMemberMutation = usePatchMember(); const { initData } = useInitDataStore(); - + const { userData } = useUserStore(); const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ communityData }: { communityData: CreateCommunity }) => createCommunity(initData, communityData), - onSuccess: async (_, communityId) => { + onSuccess: async (community) => { + await patchMemberMutation.mutateAsync({ + communityId: community?.id || "", + memberId: userData.id, + newData: { + config: { + fields: fieldsToFieldValues(community?.config.fields ?? []), + }, + id: "", + isAdmin: true, + userId: userData.id, + }, + }); await Promise.all([ queryClient.invalidateQueries({ queryKey: ["/communities", initData] }), queryClient.refetchQueries({ - queryKey: [`/communities/${communityId}`, initData], + queryKey: [`/communities/${community?.id}`, initData], }), ]); }, diff --git a/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx b/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx index 81feefd..f897fba 100644 --- a/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx @@ -4,51 +4,14 @@ import Plus from "../../../assets/plus_white.svg"; import CopyIcon from "../../../assets/copy_icon.svg"; import { openTelegramLink } from "@telegram-apps/sdk-react"; import { BOT_USERNAME } from "../../../shared/constants"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import FixedBottomButtonComponent from "../../../components/fixedBottomButton.component"; -import useCreateCommunity from "../../../hooks/communities/mutations/useCreateCommunity"; import useCommunityWithChatInfoStore from "../../../stores/create_community/communityWithChatInfo.store"; import { useNavigate } from "react-router-dom"; const CommunityWithChatConnectPage = () => { - const { fields, setChatId, chatId, description } = - useCommunityWithChatInfoStore(); - const [command] = useState(`/connect ${chatId}`); - const createCommunityMutation = useCreateCommunity(); - const [isLoading, setIsLoading] = useState(false); + const { communityId } = useCommunityWithChatInfoStore(); + const [command] = useState(`/connect ${communityId}`); const navigate = useNavigate(); - useEffect(() => { - const createCommunity = async () => { - try { - setIsLoading(true); - console.log(fields); - const community = await createCommunityMutation.mutateAsync({ - communityData: { - avatar: "", - config: { - fields: fields, - }, - description: description, - name: "", - }, - }); - - if (community) { - setChatId(community.id); - } else { - alert("Уупс, произошла ошибка"); - navigate(-1); - } - } catch (error) { - console.error("Ошибка создания сообщества:", error); - alert("Произошла ошибка при создании сообщества"); - navigate(-1); - } finally { - setIsLoading(false); - } - }; - - createCommunity(); - }, []); return ( @@ -62,23 +25,17 @@ const CommunityWithChatConnectPage = () => { stepNumber={1} value="Скопируйте команду с вашим ChatID" /> - {isLoading ? ( -
    -
    -
    - ) : ( -
    -

    {command}

    - { - navigator.clipboard.writeText(command); - }} - /> -
    - )} +
    +

    {command}

    + { + navigator.clipboard.writeText(command); + }} + /> +
    diff --git a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx index 32e63c2..d2e8a1c 100644 --- a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx @@ -27,23 +27,59 @@ import { fieldsWithIdToFields, } from "../../../mappers/fieldsToFieldsWithId"; import { FieldType } from "../../../types/fields/field.type"; +import useCreateCommunity from "../../../hooks/communities/mutations/useCreateCommunity"; +import usePatchMember from "../../../hooks/members/mutations/usePatchMember"; +import { fieldsToFieldValues } from "../../../mappers/FieldValues"; +import useUserStore from "../../../stores/user.store"; export interface ExtendedField extends Field { id: string; } const CommunityWithChatProfilePage = () => { - const { fields: storeFields, setFields: setStoreFields } = - useCommunityWithChatInfoStore(); - console.log(storeFields); + const createCommunityMutation = useCreateCommunity(); + const { userData } = useUserStore(); + const patchMemberMutation = usePatchMember(); + const { + fields: storeFields, + setFields: setStoreFields, + description, + communityId, + setCommunityId, + } = useCommunityWithChatInfoStore(); const navigate = useNavigate(); const [openedIndex, setOpenedIndex] = useState(null); + const [fields, setFields] = useState( fieldsToFieldsWithId(storeFields) ); const handleContinue = () => { + const createCommunity = async () => { + try { + const community = await createCommunityMutation.mutateAsync({ + communityData: { + avatar: "", + config: { + fields: fields, + }, + description: description, + name: "", + }, + }); + + if (community) { + setCommunityId(community.id); + navigate("/community/create/with_chat/connect_chat"); + } else { + alert("Произошла ошибка при создании сообщества"); + } + } catch (error) { + alert("Произошла ошибка при создании сообщества"); + } + }; + setStoreFields(fieldsWithIdToFields(fields)); - navigate("/community/create/with_chat/connect_chat"); + createCommunity(); }; const sensors = useSensors( @@ -167,9 +203,11 @@ const CommunityWithChatProfilePage = () => { handleContinue()} + handleClick={() => { + handleContinue(); + }} />
    diff --git a/client/src/stores/create_community/communityWithChatInfo.store.ts b/client/src/stores/create_community/communityWithChatInfo.store.ts index 9019ef9..9930863 100644 --- a/client/src/stores/create_community/communityWithChatInfo.store.ts +++ b/client/src/stores/create_community/communityWithChatInfo.store.ts @@ -5,12 +5,12 @@ interface CommunityInfoStore { description: string; fields: Field[]; chatRequired: boolean; - chatId: string; + communityId: string; setDescription: (v: string) => void; setFields: (fields: Field[]) => void; setChatRequired: (v: boolean) => void; - setChatId: (v: string) => void; + setCommunityId: (communityId: string) => void; resetStore: () => void; } @@ -20,18 +20,19 @@ const useCommunityWithChatInfoStore = create((set) => ({ fields: [], chatRequired: false, chatId: "", + communityId: "", setDescription: (v) => set({ description: v }), setFields: (fields) => set({ fields }), setChatRequired: (v) => set({ chatRequired: v }), - setChatId: (v) => set({ chatId: v }), + setCommunityId: (communityId: string) => set({ communityId: communityId }), resetStore: () => set({ description: "", fields: [], chatRequired: false, - chatId: "", + communityId: "", }), })); From 1767baf1a358d1d74e6bb10ccb7dcd3420a4db8c Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Sun, 18 May 2025 00:54:09 +0300 Subject: [PATCH 46/78] done creating communitites --- client/src/App.tsx | 19 +- client/src/api/communities.api.ts | 34 +++ client/src/components/editProfileForm.tsx | 96 -------- .../mutations/usePostAvatarCommunity.tsx | 29 +++ .../pages/communityCurrentProfile.page.tsx | 38 ++-- .../create_community/communityLinksPage.tsx | 8 + .../withChat/communityWithChatConnectPage.tsx | 1 - .../withChat/communityWithChatProfilePage.tsx | 15 +- .../communityWithoutChatProfilePage.tsx | 44 +++- client/src/pages/editProfile.page.tsx | 214 ------------------ .../src/pages/editProfileCommunity.page.tsx | 120 +++++----- .../src/pages/generalCurrentProfile.page.tsx | 82 ------- client/src/pages/invitation.page.tsx | 5 + client/src/pages/profile.page.tsx | 47 +--- client/src/shared/constants.ts | 2 +- .../community/avatar.community.interface.ts | 3 + client/src/utils/equalFields.ts | 31 ++- 17 files changed, 240 insertions(+), 548 deletions(-) delete mode 100644 client/src/components/editProfileForm.tsx create mode 100644 client/src/hooks/communities/mutations/usePostAvatarCommunity.tsx create mode 100644 client/src/pages/create_community/communityLinksPage.tsx delete mode 100644 client/src/pages/editProfile.page.tsx delete mode 100644 client/src/pages/generalCurrentProfile.page.tsx create mode 100644 client/src/types/community/avatar.community.interface.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 3148626..baea611 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -12,7 +12,6 @@ import CurrentProfilePage from "./pages/communityCurrentProfile.page"; import ProfilePage from "./pages/profile.page"; import CommunitiesPage from "./pages/communities.page"; import ProfileRedirection from "./utils/profileRedirection"; -import GeneralCurrentProfilePage from "./pages/generalCurrentProfile.page"; import RegistrationPage from "./pages/registration.page"; import InvitationPage from "./pages/invitation.page"; import EditProfileCommunityPage from "./pages/editProfileCommunity.page"; @@ -25,6 +24,8 @@ import CommunityWithChatProfilePage from "./pages/create_community/withChat/comm import CommunityWithChatDescriptionPage from "./pages/create_community/withChat/communityWithChatDescriptionPage"; import CommunityWithChatConnectPage from "./pages/create_community/withChat/communityWithChatConnectPage"; import CommunityWithoutChatProfilePage from "./pages/create_community/withoutChat/communityWithoutChatProfilePage"; +import useCommunityWithChatInfoStore from "./stores/create_community/communityWithChatInfo.store"; +import useCommunityWithoutChatInfoStore from "./stores/create_community/communityWithoutChatInfo.store"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -35,7 +36,9 @@ const pageVariants = { function App() { const navigate = useNavigate(); const location = useLocation(); - + const { resetStore: resetWithChatStore } = useCommunityWithChatInfoStore(); + const { resetStore: resetWithoutChatStore } = + useCommunityWithoutChatInfoStore(); const disableAnimation = useMemo( () => location.pathname.startsWith("/community/create/with_chat") || @@ -51,6 +54,13 @@ function App() { useEffect(() => setBackButtonHandler(), [setBackButtonHandler]); + useEffect(() => { + if (!location.pathname.startsWith("/community/create")) { + resetWithChatStore(); + resetWithoutChatStore(); + } + }, [location.pathname]); + const routes = ( }> @@ -103,11 +113,6 @@ function App() { path="/profile/current/:communityId/edit" element={} /> - } - /> - } /> => { +// try{ +// const response = await api.post(`/communities/id/${communityId}/setAvatar`), +// } +// } + +export const setCommunityAvatar = async ( + initData: string, + communityId: string, + avatar: File +): Promise => { + try { + const formData = new FormData(); + formData.append("file", avatar); + + const response = await api.post( + `/communities/id/${communityId}/set_avatar`, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + "X-Api-Token": initData, + }, + } + ); + + return response.data; + } catch (error) { + console.error("Ошибка при загрузке аватара сообщества:", error); + return null; + } +}; + export const getCommunity = async ( initData: string, id: string diff --git a/client/src/components/editProfileForm.tsx b/client/src/components/editProfileForm.tsx deleted file mode 100644 index 047f127..0000000 --- a/client/src/components/editProfileForm.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { useState } from "react"; -import { Field } from "../types/fields/field.interface"; -import { MemberConfig } from "../types/member/memberConfig.interface"; -import ButtonComponent from "./button.component"; -import EBBComponent from "./enableBackButtonComponent"; -import InputFieldComponent from "./form/inputField.component"; -import TextareaFieldComponent from "./form/textareaField.component"; -import fieldsAreEqual from "../utils/equalFields"; -import { fieldsToFieldValues } from "../mappers/FieldValues"; - -interface FormProps { - fields: Field[]; -} - -const EditProfileForm = ({ fields }: FormProps) => { - const [formChanged, setFormChanged] = useState(false); - // const patchMemberMutation = usePatchMember(); - // const navigate = useNavigate(); - const handleFieldChange = (index: number, value: string) => { - const updatedFields = [...stateFields]; - if ( - updatedFields[index].type === "textinput" && - updatedFields[index].textinput - ) { - updatedFields[index].textinput.default = value; - } else if ( - updatedFields[index].type === "textarea" && - updatedFields[index].textarea - ) { - updatedFields[index].textarea.default = value; - } - setStateFields(updatedFields); - setFormChanged(!fieldsAreEqual(updatedFields, fields)); - }; - - const handleFormSubmit = async () => { - try { - const submitData: MemberConfig = { - fields: fieldsToFieldValues(fields), - }; - console.log(submitData); - } catch (error) { - console.error("Form submission error:", error); - } - }; - - const [stateFields, setStateFields] = useState(fields); - return ( - -
    -
    { - e.preventDefault(); - }} - > - {stateFields.map((field, index) => { - if (field.type === "textarea") - return ( - - handleFieldChange(index, val) - } - /> - ); - else if (field.type === "textinput") - return ( - - handleFieldChange(index, val) - } - /> - ); - })} - -
    - handleFormSubmit()} - /> -
    - -
    -
    - ); -}; -export default EditProfileForm; diff --git a/client/src/hooks/communities/mutations/usePostAvatarCommunity.tsx b/client/src/hooks/communities/mutations/usePostAvatarCommunity.tsx new file mode 100644 index 0000000..9305052 --- /dev/null +++ b/client/src/hooks/communities/mutations/usePostAvatarCommunity.tsx @@ -0,0 +1,29 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import useInitDataStore from "../../../stores/InitData.store"; +import { setCommunityAvatar } from "../../../api/communities.api"; + +const useSetCommunityAvatar = () => { + const { initData } = useInitDataStore(); + + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + communityId, + avatar, + }: { + communityId: string; + avatar: File; + }) => setCommunityAvatar(initData, communityId, avatar), + onSuccess: async (_, communityId) => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["/communities", initData] }), + queryClient.refetchQueries({ + queryKey: [`/communities/${communityId}`, initData], + }), + ]); + }, + }); +}; + +export default useSetCommunityAvatar; diff --git a/client/src/pages/communityCurrentProfile.page.tsx b/client/src/pages/communityCurrentProfile.page.tsx index dfac042..518044e 100644 --- a/client/src/pages/communityCurrentProfile.page.tsx +++ b/client/src/pages/communityCurrentProfile.page.tsx @@ -8,17 +8,19 @@ import FixedBottomButtonComponent from "../components/fixedBottomButton.componen import useGetMe from "../hooks/users/fetchHooks/useGetMe"; import { useExtractFields } from "../hooks/utils/extractFields"; import chunkArray from "../utils/chunkArray"; +import useCommunity from "../hooks/communities/fetchHooks/useСommunity"; const CommunityCurrentProfilePage = () => { const { communityId } = useParams(); const { data, isLoading } = useGetMe(); - const fields = data?.members.find( + const { data: communityData } = useCommunity(communityId!); + + const fieldsData = data?.members.find( (member) => member.community.id === communityId )?.config.fields; + const orderedFieldsPattern = communityData?.config.fields; - const { extractedFields } = useExtractFields(fields); - const chunkedFields = chunkArray(extractedFields, 3); const navigate = useNavigate(); const goToEditProfilePage = () => { navigate(`/profile/current/${communityId}/edit`); @@ -48,28 +50,20 @@ const CommunityCurrentProfilePage = () => {
    - {chunkedFields.map((chunk, index) => { - return ( - - {chunk.map((field, index) => ( + + {orderedFieldsPattern && + orderedFieldsPattern.map((field, index) => { + return ( - ))} - - ); - })} - {/* - {extractedFields.map((field, index) => ( - - ))} - */} + ); + })} +
    diff --git a/client/src/pages/create_community/communityLinksPage.tsx b/client/src/pages/create_community/communityLinksPage.tsx new file mode 100644 index 0000000..1a5de47 --- /dev/null +++ b/client/src/pages/create_community/communityLinksPage.tsx @@ -0,0 +1,8 @@ +import useCommunityWithChatInfoStore from "../../stores/create_community/communityWithChatInfo.store"; + +const CommunityLinksPage = () => { + const { communityId: communityIdWithChat } = useCommunityWithChatInfoStore(); + <>Community Link; +}; + +export default CommunityLinksPage; diff --git a/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx b/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx index f897fba..7ce0f1d 100644 --- a/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx @@ -11,7 +11,6 @@ import { useNavigate } from "react-router-dom"; const CommunityWithChatConnectPage = () => { const { communityId } = useCommunityWithChatInfoStore(); const [command] = useState(`/connect ${communityId}`); - const navigate = useNavigate(); return ( diff --git a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx index d2e8a1c..53476a3 100644 --- a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx @@ -28,22 +28,16 @@ import { } from "../../../mappers/fieldsToFieldsWithId"; import { FieldType } from "../../../types/fields/field.type"; import useCreateCommunity from "../../../hooks/communities/mutations/useCreateCommunity"; -import usePatchMember from "../../../hooks/members/mutations/usePatchMember"; -import { fieldsToFieldValues } from "../../../mappers/FieldValues"; -import useUserStore from "../../../stores/user.store"; export interface ExtendedField extends Field { id: string; } const CommunityWithChatProfilePage = () => { const createCommunityMutation = useCreateCommunity(); - const { userData } = useUserStore(); - const patchMemberMutation = usePatchMember(); const { fields: storeFields, setFields: setStoreFields, description, - communityId, setCommunityId, } = useCommunityWithChatInfoStore(); const navigate = useNavigate(); @@ -60,7 +54,7 @@ const CommunityWithChatProfilePage = () => { communityData: { avatar: "", config: { - fields: fields, + fields: fields.filter((field) => field.title.length > 0), }, description: description, name: "", @@ -98,7 +92,6 @@ const CommunityWithChatProfilePage = () => { const handleDragEnd = (event: any) => { const { active, over } = event; - console.log("Drag event:", { active, over }); if (active.id !== over?.id) { setFields((items) => { @@ -125,8 +118,6 @@ const CommunityWithChatProfilePage = () => { }, }, ]); - - console.log(storeFields); }; const handleDelete = (index: number) => { @@ -176,17 +167,13 @@ const CommunityWithChatProfilePage = () => { handleDelete={() => handleDelete(index)} onTypeChange={(type: FieldType) => { const updated = [...fields]; - console.log(updated[index]); updated[index] = { ...updated[index], type: type, [type]: { default: "" }, }; - console.log(updated[index]); - console.log(updated); setFields([...updated]); - console.log(fields); }} /> diff --git a/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx b/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx index 8755503..e3ebbd5 100644 --- a/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx +++ b/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx @@ -27,22 +27,56 @@ import { } from "../../../mappers/fieldsToFieldsWithId"; import { FieldType } from "../../../types/fields/field.type"; import useCommunityWithoutChatInfoStore from "../../../stores/create_community/communityWithoutChatInfo.store"; +import useCreateCommunity from "../../../hooks/communities/mutations/useCreateCommunity"; +import useSetCommunityAvatar from "../../../hooks/communities/mutations/usePostAvatarCommunity"; export interface ExtendedField extends Field { id: string; } const CommunityWithoutChatProfilePage = () => { - const { fields: storeFields, setFields: setStoreFields } = - useCommunityWithoutChatInfoStore(); + const { + fields: storeFields, + setFields: setStoreFields, + description, + avatar, + name, + } = useCommunityWithoutChatInfoStore(); const navigate = useNavigate(); const [openedIndex, setOpenedIndex] = useState(null); const [fields, setFields] = useState( fieldsToFieldsWithId(storeFields) ); + const [communityId, setCommunityId] = useState(null); + const createCommunityMutation = useCreateCommunity(); + const setCommunityAvatarMutation = useSetCommunityAvatar(); + const handleContinue = async () => { + try { + const community = await createCommunityMutation.mutateAsync({ + communityData: { + avatar: "", + config: { + fields: fields.filter((field) => field.title.length > 0), + }, + description: description, + name: name, + }, + }); + + if (community) { + setCommunityId(community.id); + if (avatar) { + await setCommunityAvatarMutation.mutateAsync({ + communityId: community.id, + avatar, + }); + } - const handleContinue = () => { - setStoreFields(fieldsWithIdToFields(fields)); - navigate("/community/create/with_chat/connect_chat"); + setStoreFields(fieldsWithIdToFields(fields)); + // navigate("/community/create/with_chat/connect_chat"); + } + } catch (error) { + alert("Произошла ошибка при создании сообщества или загрузке аватара"); + } }; const sensors = useSensors( diff --git a/client/src/pages/editProfile.page.tsx b/client/src/pages/editProfile.page.tsx deleted file mode 100644 index 9873028..0000000 --- a/client/src/pages/editProfile.page.tsx +++ /dev/null @@ -1,214 +0,0 @@ -// import { useState, useEffect } from "react"; -// import InputFieldComponent from "../components/inputField.component"; -// import TextareaFieldComponent from "../components/textareaField.component"; -// import { UserData, ProfileUserData } from "../types/user.interface"; -// import { handleImageError } from "../utils/imageErrorHandler"; -// import { useNavigate } from "react-router-dom"; -// import EBBComponent from "../components/enableBackButtonComponent"; -// import DevImage from "../assets/dev.png"; -// import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; -// import ButtonComponent from "../components/button.component"; -// import useInitDataStore from "../stores/InitData.store"; -// import useMe from "../hooks/useMe"; -// import useUpdateMe from "../hooks/useUpdateMe"; - -// const EditProfilePage = () => { -// const updateMeMutation = useUpdateMe(); - -// const { launchParams } = useInitDataStore(); -// const navigate = useNavigate(); - -// const { data } = useMe(); -// const userData = { -// firstName: data?.firstName ?? null, -// lastName: data?.lastName ?? null, -// company: data?.company ?? null, -// role: data?.role ?? null, -// bio: data?.bio ?? null, -// avatar: data?.avatar ?? null, -// id: data?.id ?? null, -// telegramId: data?.telegramId ?? null, -// telegramUsername: data?.telegramUsername ?? null, -// }; - -// const [profileData, setProfileData] = useState< -// Omit -// >({ ...userData }); - -// const [isChanged, setIsChanged] = useState(false); -// const [isFilled, setIsFilled] = useState(true); -// const isProfileChanged = (newProfileData: ProfileUserData) => { -// const isChanged = -// newProfileData.firstName !== userData.firstName || -// newProfileData.lastName !== userData.lastName || -// newProfileData.company !== userData.company || -// newProfileData.role !== userData.role || -// newProfileData.bio !== userData.bio; - -// return isChanged; -// }; - -// const isProfileFilled = ( -// newProfileData: Pick< -// UserData, -// "firstName" | "lastName" | "bio" | "role" | "company" -// >, -// ) => { -// const isFilled = -// profileData.firstName?.trim() != "" && -// profileData.lastName?.trim() !== "" && -// profileData.company?.trim() !== "" && -// profileData.role?.trim() !== "" && -// newProfileData.bio?.trim() !== ""; - -// return isFilled; -// }; - -// const MAX_INPUT_LENGTH = 40; -// const MAX_TEXTAREA_LENGTH = 230; - -// const handleInputChange = (field: keyof UserData, value: string) => { -// setProfileData((prevState) => ({ -// ...prevState, -// [field]: value, -// })); -// }; -// const goBack = () => { -// navigate(-1); -// }; - -// useEffect(() => { -// setIsChanged(isProfileChanged(profileData)); -// setIsFilled(isProfileFilled(profileData)); -// }, [profileData]); - -// const [iosKeyboardOpen, setIosKeyboardOpen] = useState(false); -// const [focusedFieldsCount, setFocusedFiledsCount] = useState(0); -// const onFocusHandler = () => { -// setFocusedFiledsCount(focusedFieldsCount + 1); -// }; -// const onBlurHandler = () => { -// setFocusedFiledsCount(focusedFieldsCount - 1); -// }; -// const handleFieldsCount = () => { -// if (launchParams?.tgWebAppPlatform === "ios") { -// if (focusedFieldsCount > 0) { -// setIosKeyboardOpen(true); -// } -// if (focusedFieldsCount === 0) { -// setIosKeyboardOpen(false); -// } -// } -// }; - -// const handleClick = async () => { -// if (!isFilled) return; - -// if (isChanged) { -// try { -// await updateMeMutation.mutateAsync(profileData); -// goBack(); -// } catch (error) { -// console.error("Ошибка при обновлении профиля", error); -// } -// } else { -// goBack(); -// } -// }; - -// useEffect(handleFieldsCount, [focusedFieldsCount]); - -// return ( -// -//
    -//
    -//
    -// -//
    -//
    -// -// handleInputChange("firstName", value) -// } -// value={profileData.firstName ?? ""} -// maxLength={MAX_INPUT_LENGTH} -// /> -// handleInputChange("lastName", value)} -// value={profileData.lastName ?? ""} -// maxLength={MAX_INPUT_LENGTH} -// /> -// handleInputChange("company", value)} -// value={profileData.company ?? "null"} -// maxLength={MAX_INPUT_LENGTH} -// /> -// handleInputChange("role", value)} -// value={profileData.role ?? ""} -// maxLength={MAX_INPUT_LENGTH} -// /> -// handleInputChange("bio", value)} -// title="Описание" -// value={profileData.bio ?? ""} -// maxLength={MAX_TEXTAREA_LENGTH} -// /> -// {launchParams?.tgWebAppPlatform === "ios" && ( -//
    -// -//
    -// )} -// {launchParams?.tgWebAppPlatform !== "ios" && ( -//
    -// )} -//
    -//
    -//
    - -// {launchParams?.tgWebAppPlatform !== "ios" && ( -//
    -// -//
    -// )} -//
    -// ); -// }; -// export default EditProfilePage; - -const EditProfilePage = () => <>Edit Profile Page; -export default EditProfilePage; diff --git a/client/src/pages/editProfileCommunity.page.tsx b/client/src/pages/editProfileCommunity.page.tsx index 1a9fbba..4352ae8 100644 --- a/client/src/pages/editProfileCommunity.page.tsx +++ b/client/src/pages/editProfileCommunity.page.tsx @@ -17,12 +17,18 @@ import usePatchMember, { PatchMemberArgs, } from "../hooks/members/mutations/usePatchMember"; import { MemberConfig } from "../types/member/memberConfig.interface"; +import { FieldValue } from "../types/fields/fieldValue.interface"; +import { FieldType } from "../types/fields/field.type"; +import useCommunity from "../hooks/communities/fetchHooks/useСommunity"; +import useInitDataStore from "../stores/InitData.store"; const EditProfileCommunityPage = () => { const navigate = useNavigate(); + const { initData } = useInitDataStore(); + console.log(initData); const { communityId } = useParams(); const { data: userData, isSuccess } = useGetMe(); - + const { data: communityData } = useCommunity(communityId ?? ""); if (!isSuccess) return null; const memberUserData = userData?.members.find((memberData: Member) => { @@ -30,47 +36,40 @@ const EditProfileCommunityPage = () => { }); const fieldValues = memberUserData?.config.fields; - const fields = fieldValuesToFields(fieldValues || {}); + const orderedFieldsPattern = communityData?.config.fields; - const [stateFields, setStateFields] = useState(fields); + const [fields, setFields] = useState>( + fieldValues ?? {} + ); const [isChanged, setIsChanged] = useState(false); - const handleFieldChange = (index: number, value: string) => { - const updatedFields = [...stateFields]; - if ( - updatedFields[index].type === "textinput" && - updatedFields[index].textinput - ) { - updatedFields[index].textinput.default = value; - } else if ( - updatedFields[index].type === "textarea" && - updatedFields[index].textarea - ) { - updatedFields[index].textarea.default = value; - } - setStateFields(updatedFields); - setIsChanged(!fieldsAreEqual(updatedFields, fields)); + const handleFieldChange = (title: string, value: string, type: FieldType) => { + const updatedFields: Record = structuredClone(fields); + updatedFields[title][type]!.value = value; + setIsChanged(!fieldsAreEqual(updatedFields, fieldValues!)); + setFields(updatedFields); }; const patchMemberMutation = usePatchMember(); const handleSubmit = async () => { - const config: MemberConfig = { - fields: fieldsToFieldValues(stateFields), - }; + if (isChanged) { + const config: MemberConfig = { + fields: fields, + }; - const newMemberData: PatchMemberArgs = { - communityId: communityId ?? "", - memberId: userData?.user.id ?? "", - newData: { - config: config, - id: communityId ?? "", - isAdmin: false, - userId: userData?.user.id ?? "", - }, - }; - console.log(JSON.stringify(newMemberData)); - await patchMemberMutation.mutateAsync(newMemberData); + const newMemberData: PatchMemberArgs = { + communityId: communityId ?? "", + memberId: userData?.user.id ?? "", + newData: { + config: config, + id: communityId ?? "", + isAdmin: false, + userId: userData?.user.id ?? "", + }, + }; + await patchMemberMutation.mutateAsync(newMemberData); + } navigate(-1); }; @@ -96,33 +95,36 @@ const EditProfileCommunityPage = () => { e.preventDefault(); }} > - {stateFields.map((field, index) => { - if (field.type === "textarea") - return ( - - handleFieldChange(index, val) - } - /> - ); - else if (field.type === "textinput") - return ( - - handleFieldChange(index, val) - } - /> - ); - })} + {orderedFieldsPattern && + orderedFieldsPattern.map((field, index) => { + if (field.type === "textarea") { + return ( + + handleFieldChange(field.title, val, field.type) + } + /> + ); + } + if (field.type === "textinput") { + return ( + + handleFieldChange(field.title, val, field.type) + } + /> + ); + } + })}
    { - const { data, isLoading } = useGetMe(); - const userData = data?.user; - - const members = data?.members; - - // const navigate = useNavigate(); - // const goToEditProfilePage = () => { - // navigate("/profile/edit"); - // }; - - if (!data || isLoading) return null; - return ( - -
    -
    -
    - -
    -

    - {userData?.firstName} {userData?.lastName} -

    -

    - {`@${userData?.telegramUsername}`} -

    -
    -
    -
    - {members?.map((member, index) => ( - - ))} -
    - {/*
    - - {textInputs.map((field, index) => ( - - ))} - -
    ПОДРОБНЕЕ
    - - {textAreas.map((field, index) => ( - - ))} - -
    */} -
    -
    -
    - {/* */} -
    - ); -}; - -export default GeneralCurrentProfilePage; diff --git a/client/src/pages/invitation.page.tsx b/client/src/pages/invitation.page.tsx index 2aa0087..3df81f1 100644 --- a/client/src/pages/invitation.page.tsx +++ b/client/src/pages/invitation.page.tsx @@ -39,6 +39,11 @@ const InvitationPage = () => { Число участников: {chatData?.membersCount}

    +
    + {chatData!.description.length > 90 + ? chatData?.description.slice(0, 90) + "..." + : chatData?.description} +
    diff --git a/client/src/pages/profile.page.tsx b/client/src/pages/profile.page.tsx index b0970cd..5cb9fd8 100644 --- a/client/src/pages/profile.page.tsx +++ b/client/src/pages/profile.page.tsx @@ -8,19 +8,18 @@ import DevImage from "../assets/dev.png"; import ButtonComponent from "../components/button.component"; import TGWhite from "../assets/tg_white.svg"; import useMember from "../hooks/members/fetchHooks/useMember"; -import { useExtractFields } from "../hooks/utils/extractFields"; -import chunkArray from "../utils/chunkArray"; +import useCommunity from "../hooks/communities/fetchHooks/useСommunity"; const ProfilePage = () => { const { communityId, memberId } = useParams(); const { data, isLoading } = useMember(communityId ?? "", memberId ?? ""); - - const { extractedFields } = useExtractFields(data?.config.fields); - const chunkedFields = chunkArray(extractedFields, 3); + const { data: communityData } = useCommunity(communityId!); if (isLoading || !data) return null; const userData = data.user; + const fieldsData = data.config.fields; + const orderedFieldsPattern = communityData?.config.fields; return ( @@ -53,40 +52,18 @@ const ProfilePage = () => { />
    - {chunkedFields.map((chunk, index) => { - return ( - - {chunk.map((field, index) => ( + + {orderedFieldsPattern && + orderedFieldsPattern.map((field, index) => { + return ( - ))} - - ); - })} - {/* - {textInputs.map((field, index) => ( - - ))} + ); + })} -
    - ПОДРОБНЕЕ -
    - - {textAreas.map((field, index) => ( - - ))} - */}
    diff --git a/client/src/shared/constants.ts b/client/src/shared/constants.ts index 17fc4bf..68f7a1f 100644 --- a/client/src/shared/constants.ts +++ b/client/src/shared/constants.ts @@ -1,4 +1,4 @@ export const API_URL = window.api.API_URL; export const BOT_USERNAME = window.api.BOT_USERNAME; -export const MAX_FILE_SIZE = 5 * 1024; //bytes +export const MAX_FILE_SIZE = 5 * 1024 * 1024; //bytes export const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg"]; diff --git a/client/src/types/community/avatar.community.interface.ts b/client/src/types/community/avatar.community.interface.ts new file mode 100644 index 0000000..0c3ace5 --- /dev/null +++ b/client/src/types/community/avatar.community.interface.ts @@ -0,0 +1,3 @@ +export interface AvatarCommunity { + url: string; +} diff --git a/client/src/utils/equalFields.ts b/client/src/utils/equalFields.ts index 3094c2d..509587b 100644 --- a/client/src/utils/equalFields.ts +++ b/client/src/utils/equalFields.ts @@ -1,14 +1,21 @@ -import { Field } from "../types/fields/field.interface"; - -const fieldsAreEqual = (a: Field[], b: Field[]) => { - return a.every((field, index) => { - if (field.type === "textinput") { - return field.textinput?.default === b[index].textinput?.default; - } - if (field.type === "textarea") { - return field.textarea?.default === b[index].textarea?.default; - } - return true; - }); +import { FieldValue } from "../types/fields/fieldValue.interface"; + +const fieldsAreEqual = ( + a: Record, + b: Record +): boolean => { + const keysA = Object.keys(a); + const keysB = Object.keys(b); + + if (keysA.length !== keysB.length) return false; + + for (const key of keysA) { + if (!b[key]) return false; + if (a[key].type !== b[key].type) return false; + + const type = a[key].type; + if (a[key][type]?.value !== b[key][type]?.value) return false; + } + return true; }; export default fieldsAreEqual; From c2bec45f50f49425ac298bee5a3d94275d6eb5e2 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Sun, 18 May 2025 02:31:24 +0300 Subject: [PATCH 47/78] community creations --- client/package-lock.json | 60 ++++++++++++++ client/package.json | 2 + client/src/App.tsx | 5 ++ client/src/assets/star_buka.png | Bin 10098 -> 21432 bytes client/src/components/copyField.component.tsx | 30 +++++++ .../pages/communityCurrentProfile.page.tsx | 32 ++++---- .../create_community/communityLinksPage.tsx | 75 +++++++++++++++++- .../withChat/communityWithChatConnectPage.tsx | 22 +++-- .../communityWithoutChatProfilePage.tsx | 13 ++- .../src/pages/editProfileCommunity.page.tsx | 5 -- client/src/pages/profile.page.tsx | 26 +++--- 11 files changed, 212 insertions(+), 58 deletions(-) create mode 100644 client/src/components/copyField.component.tsx diff --git a/client/package-lock.json b/client/package-lock.json index d41092c..27409d3 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,8 +15,10 @@ "@tanstack/react-query": "^5.74.8", "@telegram-apps/sdk-react": "^3.1.2", "axios": "^1.8.3", + "html2canvas": "^1.4.1", "motion": "^12.5.0", "node": "^23.9.0", + "qrcode.react": "^4.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.56.3", @@ -2171,6 +2173,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -2409,6 +2420,15 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -3154,6 +3174,19 @@ "node": ">= 0.4" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3974,6 +4007,15 @@ "node": ">=6" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4324,6 +4366,15 @@ "node": ">=6" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4465,6 +4516,15 @@ "punycode": "^2.1.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", diff --git a/client/package.json b/client/package.json index e223f47..97e7388 100644 --- a/client/package.json +++ b/client/package.json @@ -18,8 +18,10 @@ "@tanstack/react-query": "^5.74.8", "@telegram-apps/sdk-react": "^3.1.2", "axios": "^1.8.3", + "html2canvas": "^1.4.1", "motion": "^12.5.0", "node": "^23.9.0", + "qrcode.react": "^4.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.56.3", diff --git a/client/src/App.tsx b/client/src/App.tsx index baea611..0cbf0f0 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -26,6 +26,7 @@ import CommunityWithChatConnectPage from "./pages/create_community/withChat/comm import CommunityWithoutChatProfilePage from "./pages/create_community/withoutChat/communityWithoutChatProfilePage"; import useCommunityWithChatInfoStore from "./stores/create_community/communityWithChatInfo.store"; import useCommunityWithoutChatInfoStore from "./stores/create_community/communityWithoutChatInfo.store"; +import CommunityLinksPage from "./pages/create_community/communityLinksPage"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -67,6 +68,10 @@ function App() { } /> } /> + } + /> }> } /> }> diff --git a/client/src/assets/star_buka.png b/client/src/assets/star_buka.png index 260bdb04e328a0d0a562c4962db2c3779070d7a6..8d5efe253e40d71ae8154cecc79476cc6ed35091 100644 GIT binary patch literal 21432 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 literal 10098 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 Ua8MW4`2YX_07*qoM6N<$f}9Ue=>Px# diff --git a/client/src/components/copyField.component.tsx b/client/src/components/copyField.component.tsx new file mode 100644 index 0000000..6464b67 --- /dev/null +++ b/client/src/components/copyField.component.tsx @@ -0,0 +1,30 @@ +import CopyIcon from "../assets/copy_icon.svg"; +export interface CopyFieldComponentProps { + content: string; + link?: boolean; +} +const CopyFieldComponent = ({ content, link }: CopyFieldComponentProps) => { + return ( +
    +

    + {link ? ( + + {content.length > 30 ? content.slice(0, 30) + "..." : content} + + ) : ( + content + )} +

    + { + navigator.clipboard.writeText(content); + }} + /> +
    + ); +}; + +export default CopyFieldComponent; diff --git a/client/src/pages/communityCurrentProfile.page.tsx b/client/src/pages/communityCurrentProfile.page.tsx index 518044e..be72a34 100644 --- a/client/src/pages/communityCurrentProfile.page.tsx +++ b/client/src/pages/communityCurrentProfile.page.tsx @@ -6,8 +6,6 @@ import EBBComponent from "../components/enableBackButtonComponent"; import DevImage from "../assets/dev.png"; import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; import useGetMe from "../hooks/users/fetchHooks/useGetMe"; -import { useExtractFields } from "../hooks/utils/extractFields"; -import chunkArray from "../utils/chunkArray"; import useCommunity from "../hooks/communities/fetchHooks/useСommunity"; const CommunityCurrentProfilePage = () => { @@ -50,20 +48,22 @@ const CommunityCurrentProfilePage = () => {
    - - {orderedFieldsPattern && - orderedFieldsPattern.map((field, index) => { - return ( - - ); - })} - + {orderedFieldsPattern && orderedFieldsPattern.length > 0 && ( + + {orderedFieldsPattern && + orderedFieldsPattern.map((field, index) => { + return ( + + ); + })} + + )}
    diff --git a/client/src/pages/create_community/communityLinksPage.tsx b/client/src/pages/create_community/communityLinksPage.tsx index 1a5de47..2b7c023 100644 --- a/client/src/pages/create_community/communityLinksPage.tsx +++ b/client/src/pages/create_community/communityLinksPage.tsx @@ -1,8 +1,75 @@ -import useCommunityWithChatInfoStore from "../../stores/create_community/communityWithChatInfo.store"; - +import { useNavigate, useParams } from "react-router-dom"; +import StarByak from "../../assets/star_buka.png"; +import CopyFieldComponent from "../../components/copyField.component"; +import { BOT_USERNAME } from "../../shared/constants"; +import FixedBottomButtonComponent from "../../components/fixedBottomButton.component"; +import { useState } from "react"; +import useCommunity from "../../hooks/communities/fetchHooks/useСommunity"; +import { QRCodeSVG } from "qrcode.react"; const CommunityLinksPage = () => { - const { communityId: communityIdWithChat } = useCommunityWithChatInfoStore(); - <>Community Link; + const { communityId } = useParams(); + const { data: communityData } = useCommunity(communityId!); + const [isQrShowing, setIsQrShowing] = useState(false); + const navigate = useNavigate(); + + return ( +
    + {!isQrShowing && ( +
    +
    + +
    +
    Поздравляем!
    +
    Настройка завершена
    +
    +
    + +
    +

    + Ссылка для вашего сообщества +

    + +
    + +
    { + setIsQrShowing(true); + }} + > + QRRR +
    +
    + )} + {isQrShowing && ( +
    +
    +

    {communityData?.name}

    +

    + qr-код для присоединения +

    +
    + +
    + )} + { + if (isQrShowing) { + setIsQrShowing(false); + } else { + navigate(-1); + } + }} + /> +
    + ); }; export default CommunityLinksPage; diff --git a/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx b/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx index 7ce0f1d..bf16483 100644 --- a/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx @@ -1,17 +1,18 @@ import EBBComponent from "../../../components/enableBackButtonComponent"; import StepComponent from "../../../components/step.component"; import Plus from "../../../assets/plus_white.svg"; -import CopyIcon from "../../../assets/copy_icon.svg"; import { openTelegramLink } from "@telegram-apps/sdk-react"; import { BOT_USERNAME } from "../../../shared/constants"; import { useState } from "react"; import FixedBottomButtonComponent from "../../../components/fixedBottomButton.component"; import useCommunityWithChatInfoStore from "../../../stores/create_community/communityWithChatInfo.store"; import { useNavigate } from "react-router-dom"; +import CopyFieldComponent from "../../../components/copyField.component"; const CommunityWithChatConnectPage = () => { const { communityId } = useCommunityWithChatInfoStore(); const [command] = useState(`/connect ${communityId}`); + const navigate = useNavigate(); return (
    @@ -24,17 +25,8 @@ const CommunityWithChatConnectPage = () => { stepNumber={1} value="Скопируйте команду с вашим ChatID" /> -
    -

    {command}

    - { - navigator.clipboard.writeText(command); - }} - /> -
    + +
    @@ -73,7 +65,11 @@ const CommunityWithChatConnectPage = () => { {}} + handleClick={() => { + navigate(`/communities`, { replace: true }); + navigate(`/profile/current/${communityId}/edit`); + navigate(`/community/${communityId}/links`); + }} />
    diff --git a/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx b/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx index e3ebbd5..0cd21df 100644 --- a/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx +++ b/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx @@ -21,10 +21,7 @@ import { import { v4 as uuidv4 } from "uuid"; import FixedBottomButtonComponent from "../../../components/fixedBottomButton.component"; import { SortableItem } from "../../../components/sortableItem"; -import { - fieldsToFieldsWithId, - fieldsWithIdToFields, -} from "../../../mappers/fieldsToFieldsWithId"; +import { fieldsToFieldsWithId } from "../../../mappers/fieldsToFieldsWithId"; import { FieldType } from "../../../types/fields/field.type"; import useCommunityWithoutChatInfoStore from "../../../stores/create_community/communityWithoutChatInfo.store"; import useCreateCommunity from "../../../hooks/communities/mutations/useCreateCommunity"; @@ -36,7 +33,6 @@ export interface ExtendedField extends Field { const CommunityWithoutChatProfilePage = () => { const { fields: storeFields, - setFields: setStoreFields, description, avatar, name, @@ -46,7 +42,6 @@ const CommunityWithoutChatProfilePage = () => { const [fields, setFields] = useState( fieldsToFieldsWithId(storeFields) ); - const [communityId, setCommunityId] = useState(null); const createCommunityMutation = useCreateCommunity(); const setCommunityAvatarMutation = useSetCommunityAvatar(); const handleContinue = async () => { @@ -63,7 +58,6 @@ const CommunityWithoutChatProfilePage = () => { }); if (community) { - setCommunityId(community.id); if (avatar) { await setCommunityAvatarMutation.mutateAsync({ communityId: community.id, @@ -71,7 +65,10 @@ const CommunityWithoutChatProfilePage = () => { }); } - setStoreFields(fieldsWithIdToFields(fields)); + navigate(`/communities`, { replace: true }); + navigate(`/community/${community.id}`); + navigate(`/community/${community.id}/links`); + // navigate("/community/create/with_chat/connect_chat"); } } catch (error) { diff --git a/client/src/pages/editProfileCommunity.page.tsx b/client/src/pages/editProfileCommunity.page.tsx index 4352ae8..e8d698b 100644 --- a/client/src/pages/editProfileCommunity.page.tsx +++ b/client/src/pages/editProfileCommunity.page.tsx @@ -7,11 +7,6 @@ import TextareaFieldComponent from "../components/form/textareaField.component"; import { useNavigate, useParams } from "react-router-dom"; import { Member } from "../types/member/member.interface"; import { useState } from "react"; -import { Field } from "../types/fields/field.interface"; -import { - fieldsToFieldValues, - fieldValuesToFields, -} from "../mappers/FieldValues"; import fieldsAreEqual from "../utils/equalFields"; import usePatchMember, { PatchMemberArgs, diff --git a/client/src/pages/profile.page.tsx b/client/src/pages/profile.page.tsx index 5cb9fd8..601fce4 100644 --- a/client/src/pages/profile.page.tsx +++ b/client/src/pages/profile.page.tsx @@ -52,18 +52,20 @@ const ProfilePage = () => { />
    - - {orderedFieldsPattern && - orderedFieldsPattern.map((field, index) => { - return ( - - ); - })} - + {orderedFieldsPattern && orderedFieldsPattern?.length > 0 && ( + + {orderedFieldsPattern && + orderedFieldsPattern.map((field, index) => { + return ( + + ); + })} + + )}
    From fb0cd10d8c0eea830b9dcf2837259d906b312fd6 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Sun, 18 May 2025 12:22:58 +0300 Subject: [PATCH 48/78] fixed registration form and invite<->communities redirection --- .../requireMembership.component.tsx | 2 +- .../withChat/communityWithChatConnectPage.tsx | 1 + .../communityWithoutChatProfilePage.tsx | 3 +- client/src/pages/registration.page.tsx | 106 +++++++++--------- 4 files changed, 57 insertions(+), 55 deletions(-) diff --git a/client/src/components/requireMembership.component.tsx b/client/src/components/requireMembership.component.tsx index cf923fd..d455434 100644 --- a/client/src/components/requireMembership.component.tsx +++ b/client/src/components/requireMembership.component.tsx @@ -19,7 +19,7 @@ const RequireMembershipComponent = (props: RequireMembershipComponentProps) => { useEffect(() => { if (!isRefetching && isSuccess && !data?.isMember) { - navigate("/invite"); + navigate("/invite", { replace: true }); } }, [isSuccess, data]); diff --git a/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx b/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx index bf16483..7f0ca12 100644 --- a/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatConnectPage.tsx @@ -67,6 +67,7 @@ const CommunityWithChatConnectPage = () => { state={true ? "active" : "disabled"} handleClick={() => { navigate(`/communities`, { replace: true }); + navigate(`/profile/current/${communityId}`); navigate(`/profile/current/${communityId}/edit`); navigate(`/community/${communityId}/links`); }} diff --git a/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx b/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx index 0cd21df..7255715 100644 --- a/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx +++ b/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx @@ -66,7 +66,8 @@ const CommunityWithoutChatProfilePage = () => { } navigate(`/communities`, { replace: true }); - navigate(`/community/${community.id}`); + navigate(`/profile/current/${community.id}`); + navigate(`/profile/current/${community.id}/edit`); navigate(`/community/${community.id}/links`); // navigate("/community/create/with_chat/connect_chat"); diff --git a/client/src/pages/registration.page.tsx b/client/src/pages/registration.page.tsx index ff9d494..14f5b54 100644 --- a/client/src/pages/registration.page.tsx +++ b/client/src/pages/registration.page.tsx @@ -13,6 +13,9 @@ import InputFieldComponent from "../components/form/inputField.component"; import TextareaFieldComponent from "../components/form/textareaField.component"; import useJoinCommunity from "../hooks/communities/mutations/useJoinCommunity"; import { useNavigate } from "react-router-dom"; +import { FieldType } from "../types/fields/field.type"; +import { FieldValue } from "../types/fields/fieldValue.interface"; +import fieldsAreEqual from "../utils/equalFields"; const RegistrationPage = () => { const { initDataStartParam: communityId } = useInitDataStore(); @@ -23,42 +26,35 @@ const RegistrationPage = () => { if (isPending || isLoading) return null; - const fields = communityData?.config.fields; - - const [stateFields, setStateFields] = useState(fields ?? []); + const orderedFieldsPattern = communityData?.config.fields; + const baseValues = fieldsToFieldValues(orderedFieldsPattern!); + const [fields, setFields] = useState>(baseValues); const [isChanged, setIsChanged] = useState(false); - const handleFieldChange = (index: number, value: string) => { - const updatedFields = [...stateFields]; - if ( - updatedFields[index].type === "textinput" && - updatedFields[index].textinput - ) { - updatedFields[index].textinput.default = value; - } else if ( - updatedFields[index].type === "textarea" && - updatedFields[index].textarea - ) { - updatedFields[index].textarea.default = value; - } - setStateFields(updatedFields); - setIsChanged(fieldsNotEmpty(fields ?? [])); + const handleFieldChange = (title: string, value: string, type: FieldType) => { + const updatedFields: Record = structuredClone(fields); + updatedFields[title][type]!.value = value; + setIsChanged(!fieldsAreEqual(updatedFields, baseValues)); + console.log(isChanged); + setFields(updatedFields); }; const joinCommunityMutation = useJoinCommunity(); const navigate = useNavigate(); const handleSubmit = async () => { - const config: MemberConfig = { - fields: fieldsToFieldValues(stateFields), - }; + if (isChanged) { + const config: MemberConfig = { + fields: fields, + }; - await joinCommunityMutation.mutateAsync({ - communityId: communityId ?? "", - memberConfig: config, - }); - navigate(`/communities`, { replace: true }); - navigate(`/community/${communityId}`); + await joinCommunityMutation.mutateAsync({ + communityId: communityId ?? "", + memberConfig: config, + }); + navigate(`/communities`, { replace: true }); + navigate(`/community/${communityId}`); + } }; return ( @@ -81,32 +77,36 @@ const RegistrationPage = () => { e.preventDefault(); }} > - {stateFields.map((field, index) => { - if (field.type === "textarea") - return ( - - handleFieldChange(index, val) - } - /> - ); - else if (field.type === "textinput") - return ( - - handleFieldChange(index, val) - } - /> - ); - })} + {orderedFieldsPattern && + orderedFieldsPattern.map((field, index) => { + if (field.type === "textarea") { + return ( + + handleFieldChange(field.title, val, field.type) + } + /> + ); + } + + if (field.type === "textinput") { + return ( + + handleFieldChange(field.title, val, field.type) + } + /> + ); + } + })}
    Date: Sun, 18 May 2025 12:31:35 +0300 Subject: [PATCH 49/78] fix lint --- client/src/pages/registration.page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/pages/registration.page.tsx b/client/src/pages/registration.page.tsx index 14f5b54..077f0d3 100644 --- a/client/src/pages/registration.page.tsx +++ b/client/src/pages/registration.page.tsx @@ -5,9 +5,7 @@ import { handleImageError } from "../utils/imageErrorHandler"; import DevImage from "../assets/dev.png"; import { useState } from "react"; import { fieldsToFieldValues } from "../mappers/FieldValues"; -import { Field } from "../types/fields/field.interface"; import { MemberConfig } from "../types/member/memberConfig.interface"; -import fieldsNotEmpty from "../utils/fieldsNotEmpty"; import ButtonComponent from "../components/button.component"; import InputFieldComponent from "../components/form/inputField.component"; import TextareaFieldComponent from "../components/form/textareaField.component"; From c56e147e46f56c3b53cce72e7ba8c4b0ab1cd859 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Sun, 18 May 2025 12:55:47 +0300 Subject: [PATCH 50/78] member card design fixes --- .../components/chatMember.card.component.tsx | 32 ++++++++++++++----- .../withChat/communityWithChatProfilePage.tsx | 2 +- .../src/pages/editProfileCommunity.page.tsx | 16 +++++----- client/src/pages/invitation.page.tsx | 2 +- client/src/pages/registration.page.tsx | 16 +++++----- 5 files changed, 42 insertions(+), 26 deletions(-) diff --git a/client/src/components/chatMember.card.component.tsx b/client/src/components/chatMember.card.component.tsx index aefe218..b532868 100644 --- a/client/src/components/chatMember.card.component.tsx +++ b/client/src/components/chatMember.card.component.tsx @@ -48,7 +48,13 @@ const ChatMemberCardComponent = (props: { onAnimationComplete={props.onAnimationComplete} >
    -
    +
    0 + ? `flex w-full gap-[10px] items-center justify-between flex-row pb-[10px]` + : `flex w-full gap-[10px] items-center justify-between flex-row ` + } + >
    -

    - {textArea.length > 90 ? textArea.slice(0, 90) + "..." : textArea} -

    + {textArea.length > 0 && ( +

    + {textArea.length > 90 ? textArea.slice(0, 90) + "..." : textArea} +

    + )}
    ); @@ -86,7 +94,13 @@ const ChatMemberCardComponent = (props: { onClick={props.onClick} >
    -
    +
    0 + ? `flex w-full gap-[10px] items-center justify-between flex-row pb-[10px]` + : `flex w-full gap-[10px] items-center justify-between flex-row ` + } + >
    -

    - {textArea.length > 90 ? textArea.slice(0, 90) + "..." : textArea} -

    + {textArea.length > 0 && ( +

    + {textArea.length > 90 ? textArea.slice(0, 90) + "..." : textArea} +

    + )}
    ); diff --git a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx index 53476a3..2291c54 100644 --- a/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx +++ b/client/src/pages/create_community/withChat/communityWithChatProfilePage.tsx @@ -57,7 +57,7 @@ const CommunityWithChatProfilePage = () => { fields: fields.filter((field) => field.title.length > 0), }, description: description, - name: "", + name: "Без названия", }, }); diff --git a/client/src/pages/editProfileCommunity.page.tsx b/client/src/pages/editProfileCommunity.page.tsx index e8d698b..7b09d1d 100644 --- a/client/src/pages/editProfileCommunity.page.tsx +++ b/client/src/pages/editProfileCommunity.page.tsx @@ -1,7 +1,6 @@ import useGetMe from "../hooks/users/fetchHooks/useGetMe"; import { handleImageError } from "../utils/imageErrorHandler"; import DevImage from "../assets/dev.png"; -import ButtonComponent from "../components/button.component"; import InputFieldComponent from "../components/form/inputField.component"; import TextareaFieldComponent from "../components/form/textareaField.component"; import { useNavigate, useParams } from "react-router-dom"; @@ -16,6 +15,7 @@ import { FieldValue } from "../types/fields/fieldValue.interface"; import { FieldType } from "../types/fields/field.type"; import useCommunity from "../hooks/communities/fetchHooks/useСommunity"; import useInitDataStore from "../stores/InitData.store"; +import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; const EditProfileCommunityPage = () => { const navigate = useNavigate(); @@ -85,6 +85,8 @@ const EditProfileCommunityPage = () => {
    { e.preventDefault(); @@ -120,13 +122,11 @@ const EditProfileCommunityPage = () => { ); } })} -
    - handleSubmit()} - /> -
    + handleSubmit()} + />
    diff --git a/client/src/pages/invitation.page.tsx b/client/src/pages/invitation.page.tsx index 3df81f1..442be84 100644 --- a/client/src/pages/invitation.page.tsx +++ b/client/src/pages/invitation.page.tsx @@ -4,6 +4,7 @@ import { useNavigate } from "react-router-dom"; import DevImage from "../assets/dev.png"; import useInitDataStore from "../stores/InitData.store"; import useCommunityPreview from "../hooks/communities/fetchHooks/useCommunityPreview"; +import { useState } from "react"; const InvitationPage = () => { const navigate = useNavigate(); @@ -14,7 +15,6 @@ const InvitationPage = () => { }; const { data: chatData } = useCommunityPreview(communityId ?? ""); - return (
    diff --git a/client/src/pages/registration.page.tsx b/client/src/pages/registration.page.tsx index 077f0d3..55a8668 100644 --- a/client/src/pages/registration.page.tsx +++ b/client/src/pages/registration.page.tsx @@ -6,7 +6,6 @@ import DevImage from "../assets/dev.png"; import { useState } from "react"; import { fieldsToFieldValues } from "../mappers/FieldValues"; import { MemberConfig } from "../types/member/memberConfig.interface"; -import ButtonComponent from "../components/button.component"; import InputFieldComponent from "../components/form/inputField.component"; import TextareaFieldComponent from "../components/form/textareaField.component"; import useJoinCommunity from "../hooks/communities/mutations/useJoinCommunity"; @@ -14,6 +13,7 @@ import { useNavigate } from "react-router-dom"; import { FieldType } from "../types/fields/field.type"; import { FieldValue } from "../types/fields/fieldValue.interface"; import fieldsAreEqual from "../utils/equalFields"; +import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; const RegistrationPage = () => { const { initDataStartParam: communityId } = useInitDataStore(); @@ -70,6 +70,8 @@ const RegistrationPage = () => {
    { e.preventDefault(); @@ -106,13 +108,11 @@ const RegistrationPage = () => { } })} -
    - handleSubmit()} - /> -
    + handleSubmit()} + />
    From 7267e62fec9c7d1b0b96dcf85718ce2039a39423 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Sun, 18 May 2025 13:33:45 +0300 Subject: [PATCH 51/78] aaaaaaaaaaaa i've gone insane --- client/src/App.tsx | 6 ++- .../src/components/chatPreview.component.tsx | 5 ++- .../components/settingsButtonComponent.tsx | 22 ++++++++++ client/src/pages/community.page.tsx | 5 +++ .../communitySettingsPage.tsx | 44 +++++++++++++++++++ client/src/pages/invitation.page.tsx | 1 - 6 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 client/src/components/settingsButtonComponent.tsx create mode 100644 client/src/pages/community_settings/communitySettingsPage.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 0cbf0f0..6f140a3 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -27,6 +27,7 @@ import CommunityWithoutChatProfilePage from "./pages/create_community/withoutCha import useCommunityWithChatInfoStore from "./stores/create_community/communityWithChatInfo.store"; import useCommunityWithoutChatInfoStore from "./stores/create_community/communityWithoutChatInfo.store"; import CommunityLinksPage from "./pages/create_community/communityLinksPage"; +import CommunitySettingsPage from "./pages/community_settings/communitySettingsPage"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -67,7 +68,10 @@ function App() { }> } /> } /> - + } + /> } diff --git a/client/src/components/chatPreview.component.tsx b/client/src/components/chatPreview.component.tsx index 41d7376..9a07887 100644 --- a/client/src/components/chatPreview.component.tsx +++ b/client/src/components/chatPreview.component.tsx @@ -45,7 +45,10 @@ const ChatPreviewComponent = ( props.className } > -
    +
    void; + content: string; +} +const SettingsButtonComponent = ({ + onClick, + content, +}: SettingsButtonComponentProps) => { + return ( +
    onClick()} + className="flex rounded-[20px] py-[24px] px-[24px] gap-[10px] bg-[#F5F5F5] items-center justify-between flex-row" + > +
    {content}
    + +
    + ); +}; + +export default SettingsButtonComponent; diff --git a/client/src/pages/community.page.tsx b/client/src/pages/community.page.tsx index bb62e84..057d662 100644 --- a/client/src/pages/community.page.tsx +++ b/client/src/pages/community.page.tsx @@ -40,6 +40,10 @@ const CommunityPage = () => { navigate(`/profile/current/${communityId}`); }; + const goToCommunitySettings = () => { + navigate(`/community/${communityId}/settings`); + }; + const { data: previewChatData, isSuccess } = useCommunity(communityId ?? ""); const scrollContainerRef = useRef(null); @@ -77,6 +81,7 @@ const CommunityPage = () => { view={true} animated={true} index={0} + onClick={() => goToCommunitySettings()} />
    goToMyProfile()} /> diff --git a/client/src/pages/community_settings/communitySettingsPage.tsx b/client/src/pages/community_settings/communitySettingsPage.tsx new file mode 100644 index 0000000..51c1a71 --- /dev/null +++ b/client/src/pages/community_settings/communitySettingsPage.tsx @@ -0,0 +1,44 @@ +import { useNavigate, useParams } from "react-router-dom"; +import EBBComponent from "../../components/enableBackButtonComponent"; +import SettingsButtonComponent from "../../components/settingsButtonComponent"; +const CommunitySettingsPage = () => { + const { communityId } = useParams(); + const navigate = useNavigate(); + const goToProfileSettings = () => { + navigate(`/community/${communityId}/settings/profile`); + }; + + const goToChatConnectionSettings = () => { + navigate(`/community/${communityId}/settings/chat`); + }; + + const goToDescriptionSettings = () => { + navigate(`/community/${communityId}/settings/description`); + }; + + return ( + +
    +
    +

    Настройки сообщества

    +
    +
    + {}} + /> + {}} + /> + {}} + /> +
    +
    +
    + ); +}; + +export default CommunitySettingsPage; diff --git a/client/src/pages/invitation.page.tsx b/client/src/pages/invitation.page.tsx index 442be84..23a93c4 100644 --- a/client/src/pages/invitation.page.tsx +++ b/client/src/pages/invitation.page.tsx @@ -4,7 +4,6 @@ import { useNavigate } from "react-router-dom"; import DevImage from "../assets/dev.png"; import useInitDataStore from "../stores/InitData.store"; import useCommunityPreview from "../hooks/communities/fetchHooks/useCommunityPreview"; -import { useState } from "react"; const InvitationPage = () => { const navigate = useNavigate(); From 057ef7c1b8e8077aab0446827fb48e392633bacf Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Sun, 18 May 2025 15:04:45 +0300 Subject: [PATCH 52/78] admin panel boilerplate --- client/src/App.tsx | 20 ++ client/src/api/communities.api.ts | 6 - .../mutations/usePatchCommunity.ts | 41 ++++ .../communitySettingsChatPage.tsx | 7 + .../communitySettingsDescriptionPage.tsx | 7 + .../communitySettingsLinksPage.tsx | 72 +++++++ .../communitySettingsPage.tsx | 52 +++-- .../communitySettingsProfilePage.tsx | 186 ++++++++++++++++++ .../communityWithoutChatProfilePage.tsx | 1 - .../src/pages/editProfileCommunity.page.tsx | 5 +- client/src/pages/registration.page.tsx | 1 - 11 files changed, 377 insertions(+), 21 deletions(-) create mode 100644 client/src/hooks/communities/mutations/usePatchCommunity.ts create mode 100644 client/src/pages/community_settings/communitySettingsChatPage.tsx create mode 100644 client/src/pages/community_settings/communitySettingsDescriptionPage.tsx create mode 100644 client/src/pages/community_settings/communitySettingsLinksPage.tsx create mode 100644 client/src/pages/community_settings/communitySettingsProfilePage.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 6f140a3..4d15950 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -28,6 +28,10 @@ import useCommunityWithChatInfoStore from "./stores/create_community/communityWi import useCommunityWithoutChatInfoStore from "./stores/create_community/communityWithoutChatInfo.store"; import CommunityLinksPage from "./pages/create_community/communityLinksPage"; import CommunitySettingsPage from "./pages/community_settings/communitySettingsPage"; +import CommunitySettingsProfilePage from "./pages/community_settings/communitySettingsProfilePage"; +import CommunitySettingsChatPage from "./pages/community_settings/communitySettingsChatPage"; +import CommunitySettingsDescriptionPage from "./pages/community_settings/communitySettingsDescriptionPage"; +import CommunitySettingsLinksPage from "./pages/community_settings/communitySettingsLinksPage"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -72,6 +76,22 @@ function App() { path="/community/:communityId/settings" element={} /> + } + /> + } + /> + } + /> + } + /> } diff --git a/client/src/api/communities.api.ts b/client/src/api/communities.api.ts index cf30455..2fecb62 100644 --- a/client/src/api/communities.api.ts +++ b/client/src/api/communities.api.ts @@ -55,12 +55,6 @@ export const patchCommunity = async ( } }; -// export const setCommunityAvatar = async (initData: string, communityId: string, avatar: File): Promise => { -// try{ -// const response = await api.post(`/communities/id/${communityId}/setAvatar`), -// } -// } - export const setCommunityAvatar = async ( initData: string, communityId: string, diff --git a/client/src/hooks/communities/mutations/usePatchCommunity.ts b/client/src/hooks/communities/mutations/usePatchCommunity.ts new file mode 100644 index 0000000..d635ae0 --- /dev/null +++ b/client/src/hooks/communities/mutations/usePatchCommunity.ts @@ -0,0 +1,41 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { patchCommunity } from "../../../api/communities.api"; +import { PatchCommunity } from "../../../types/postApiTypes/patchCommunity.interface"; +import { fieldsToFieldValues } from "../../../mappers/FieldValues"; +import useInitDataStore from "../../../stores/InitData.store"; +import useUserStore from "../../../stores/user.store"; +import usePatchMember from "../../members/mutations/usePatchMember"; + +const usePatchCommunity = () => { + const patchMemberMutation = usePatchMember(); + const { initData } = useInitDataStore(); + const { userData } = useUserStore(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ communityData }: { communityData: PatchCommunity }) => + patchCommunity(initData, communityData), + onSuccess: async (community) => { + await patchMemberMutation.mutateAsync({ + communityId: community?.id || "", + memberId: userData.id, + newData: { + config: { + fields: fieldsToFieldValues(community?.config.fields ?? []), + }, + id: "", + isAdmin: true, + userId: userData.id, + }, + }); + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["/communities", initData] }), + queryClient.refetchQueries({ + queryKey: [`/communities/${community?.id}`, initData], + }), + ]); + }, + }); +}; + +export default usePatchCommunity; diff --git a/client/src/pages/community_settings/communitySettingsChatPage.tsx b/client/src/pages/community_settings/communitySettingsChatPage.tsx new file mode 100644 index 0000000..64ada8b --- /dev/null +++ b/client/src/pages/community_settings/communitySettingsChatPage.tsx @@ -0,0 +1,7 @@ +import EBBComponent from "../../components/enableBackButtonComponent"; + +const CommunitySettingsChatPage = () => { + return Community; +}; + +export default CommunitySettingsChatPage; diff --git a/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx b/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx new file mode 100644 index 0000000..f0b4a57 --- /dev/null +++ b/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx @@ -0,0 +1,7 @@ +import EBBComponent from "../../components/enableBackButtonComponent"; + +const CommunitySettingsDescriptionPage = () => { + return Settings Description Page; +}; + +export default CommunitySettingsDescriptionPage; diff --git a/client/src/pages/community_settings/communitySettingsLinksPage.tsx b/client/src/pages/community_settings/communitySettingsLinksPage.tsx new file mode 100644 index 0000000..f834247 --- /dev/null +++ b/client/src/pages/community_settings/communitySettingsLinksPage.tsx @@ -0,0 +1,72 @@ +import { useNavigate, useParams } from "react-router-dom"; +import StarByak from "../../assets/star_buka.png"; +import CopyFieldComponent from "../../components/copyField.component"; +import { BOT_USERNAME } from "../../shared/constants"; +import FixedBottomButtonComponent from "../../components/fixedBottomButton.component"; +import { useState } from "react"; +import useCommunity from "../../hooks/communities/fetchHooks/useСommunity"; +import { QRCodeSVG } from "qrcode.react"; +const CommunitySettingsLinksPage = () => { + const { communityId } = useParams(); + const { data: communityData } = useCommunity(communityId!); + const [isQrShowing, setIsQrShowing] = useState(false); + const navigate = useNavigate(); + + return ( +
    + {!isQrShowing && ( +
    +
    + +
    +
    + +
    +

    + Ссылка для вашего сообщества +

    + +
    + +
    { + setIsQrShowing(true); + }} + > + QRRR +
    +
    + )} + {isQrShowing && ( +
    +
    +

    {communityData?.name}

    +

    + qr-код для присоединения +

    +
    + +
    + )} + { + if (isQrShowing) { + setIsQrShowing(false); + } else { + navigate(-1); + } + }} + /> +
    + ); +}; + +export default CommunitySettingsLinksPage; diff --git a/client/src/pages/community_settings/communitySettingsPage.tsx b/client/src/pages/community_settings/communitySettingsPage.tsx index 51c1a71..eaca0f4 100644 --- a/client/src/pages/community_settings/communitySettingsPage.tsx +++ b/client/src/pages/community_settings/communitySettingsPage.tsx @@ -1,11 +1,27 @@ import { useNavigate, useParams } from "react-router-dom"; import EBBComponent from "../../components/enableBackButtonComponent"; import SettingsButtonComponent from "../../components/settingsButtonComponent"; +import useCommunity from "../../hooks/communities/fetchHooks/useСommunity"; +import useUserStore from "../../stores/user.store"; + const CommunitySettingsPage = () => { const { communityId } = useParams(); + const { userData } = useUserStore(); + const { data: communityData, isPending } = useCommunity(communityId!); + const navigate = useNavigate(); + + if (isPending || !communityData) return null; + + const currentMember = communityData.members?.find( + (member) => member.user.id === userData.id + ); + const isAdmin = currentMember?.isAdmin === true; + const goToProfileSettings = () => { - navigate(`/community/${communityId}/settings/profile`); + if (isAdmin) { + navigate(`/community/${communityId}/settings/profile`); + } }; const goToChatConnectionSettings = () => { @@ -16,24 +32,38 @@ const CommunitySettingsPage = () => { navigate(`/community/${communityId}/settings/description`); }; + const goToLinks = () => { + navigate(`/community/${communityId}/settings/links`); + }; + return (
    -
    -

    Настройки сообщества

    +
    +

    + Настройки сообщества +

    + {isAdmin && ( + <> + + + + )} {}} - /> - {}} + content="Основная информация" + onClick={goToDescriptionSettings} /> {}} + content="Поделиться сообществом" + onClick={goToLinks} />
    diff --git a/client/src/pages/community_settings/communitySettingsProfilePage.tsx b/client/src/pages/community_settings/communitySettingsProfilePage.tsx new file mode 100644 index 0000000..8f0e9da --- /dev/null +++ b/client/src/pages/community_settings/communitySettingsProfilePage.tsx @@ -0,0 +1,186 @@ +import { useNavigate, useParams } from "react-router-dom"; +import Plus from "../../assets/plus.svg"; +import { useState } from "react"; +import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; +import { + DndContext, + closestCenter, + useSensor, + useSensors, + PointerSensor, + TouchSensor, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { v4 as uuidv4 } from "uuid"; +import CommunityProfileField from "../../components/communityProfileField"; +import EBBComponent from "../../components/enableBackButtonComponent"; +import FixedBottomButtonComponent from "../../components/fixedBottomButton.component"; +import { SortableItem } from "../../components/sortableItem"; +import { fieldsToFieldsWithId } from "../../mappers/fieldsToFieldsWithId"; +import { Field } from "../../types/fields/field.interface"; +import { FieldType } from "../../types/fields/field.type"; +import useCommunity from "../../hooks/communities/fetchHooks/useСommunity"; +import usePatchCommunity from "../../hooks/communities/mutations/usePatchCommunity"; +export interface ExtendedField extends Field { + id: string; +} +const CommunitySettingsProfilePage = () => { + const patchCommunityMutation = usePatchCommunity(); + const { communityId } = useParams(); + const { data, isPending } = useCommunity(communityId!); + const storeFields = data?.config.fields; + const description = data?.description; + const name = data?.name; + const tgChatId = data?.tgChatID; + const navigate = useNavigate(); + const [openedIndex, setOpenedIndex] = useState(null); + const [fields, setFields] = useState( + fieldsToFieldsWithId(storeFields!) + ); + const handleContinue = async () => { + try { + const community = await patchCommunityMutation.mutateAsync({ + communityData: { + avatar: "", + config: { + fields: fields.filter((field) => field.title.length > 0), + }, + description: description!, + name: name!, + id: communityId!, + tgChatID: tgChatId!, + }, + }); + + if (community) { + navigate(-1); + } + } catch (error) { + alert("Произошла ошибка при создании сообщества или загрузке аватара"); + } + }; + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 150, + tolerance: 10, + }, + }) + ); + + const handleDragEnd = (event: any) => { + const { active, over } = event; + + if (active.id !== over?.id) { + setFields((items) => { + const oldIndex = items.findIndex((item) => item.id === active.id); + const newIndex = items.findIndex((item) => item.id === over.id); + return arrayMove(items, oldIndex, newIndex); + }); + if (openedIndex !== null) { + setOpenedIndex(null); + } + } + }; + + const addNewField = () => { + setFields([ + ...fields, + { + id: uuidv4(), + description: "", + title: "", + type: "textinput", + }, + ]); + }; + + const handleDelete = (index: number) => { + const updated = [...fields]; + updated.splice(index, 1); + setFields(updated); + if (openedIndex === index) setOpenedIndex(null); + }; + + if (isPending) return null; + + return ( + +
    +
    +

    Профиль участника

    + + Создайте поля для заполнения информации о приглашённых участниках и + задайте их размер + +
    + + + field.id)} + strategy={verticalListSortingStrategy} + > +
    +
    +

    Имя, фамилия

    +
    + {fields.map((field, index) => ( + + { + const updated = [...fields]; + updated[index] = { ...updated[index], title: newValue }; + setFields(updated); + }} + type={field.type} + isOpen={openedIndex === index} + onOpen={() => setOpenedIndex(index)} + onClose={() => setOpenedIndex(null)} + handleDelete={() => handleDelete(index)} + onTypeChange={(type: FieldType) => { + const updated = [...fields]; + updated[index] = { ...updated[index], type: type }; + setFields(updated); + }} + /> + + ))} + +
    + +
    +
    +
    +
    + + handleContinue()} + /> +
    +
    +
    + ); +}; + +export default CommunitySettingsProfilePage; diff --git a/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx b/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx index 7255715..cb4ead8 100644 --- a/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx +++ b/client/src/pages/create_community/withoutChat/communityWithoutChatProfilePage.tsx @@ -93,7 +93,6 @@ const CommunityWithoutChatProfilePage = () => { const handleDragEnd = (event: any) => { const { active, over } = event; - console.log("Drag event:", { active, over }); if (active.id !== over?.id) { setFields((items) => { diff --git a/client/src/pages/editProfileCommunity.page.tsx b/client/src/pages/editProfileCommunity.page.tsx index 7b09d1d..a2ab05d 100644 --- a/client/src/pages/editProfileCommunity.page.tsx +++ b/client/src/pages/editProfileCommunity.page.tsx @@ -20,7 +20,6 @@ import FixedBottomButtonComponent from "../components/fixedBottomButton.componen const EditProfileCommunityPage = () => { const navigate = useNavigate(); const { initData } = useInitDataStore(); - console.log(initData); const { communityId } = useParams(); const { data: userData, isSuccess } = useGetMe(); const { data: communityData } = useCommunity(communityId ?? ""); @@ -59,7 +58,9 @@ const EditProfileCommunityPage = () => { newData: { config: config, id: communityId ?? "", - isAdmin: false, + isAdmin: communityData?.members.find( + (member) => member.user.id == userData?.user.id + )?.isAdmin!, userId: userData?.user.id ?? "", }, }; diff --git a/client/src/pages/registration.page.tsx b/client/src/pages/registration.page.tsx index 55a8668..16471f8 100644 --- a/client/src/pages/registration.page.tsx +++ b/client/src/pages/registration.page.tsx @@ -34,7 +34,6 @@ const RegistrationPage = () => { const updatedFields: Record = structuredClone(fields); updatedFields[title][type]!.value = value; setIsChanged(!fieldsAreEqual(updatedFields, baseValues)); - console.log(isChanged); setFields(updatedFields); }; From d1f39e73f4e357a4298746d0cea04e5d643f074c Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Sun, 18 May 2025 15:50:03 +0300 Subject: [PATCH 53/78] edit community with chat description --- client/src/App.tsx | 13 ++++ .../src/components/chatPreview.component.tsx | 18 ++++- .../communitySettingsDescriptionPage.tsx | 60 ++++++++++++++- ...ityWithChatSettingsDescriptionEditPage.tsx | 73 +++++++++++++++++++ ...WithoutChatSettingsDescriptionEditPage.tsx | 7 ++ 5 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 client/src/pages/community_settings/communityWithChatSettingsDescriptionEditPage.tsx create mode 100644 client/src/pages/community_settings/communityWithoutChatSettingsDescriptionEditPage.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 4d15950..f3fb0b2 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -32,6 +32,8 @@ import CommunitySettingsProfilePage from "./pages/community_settings/communitySe import CommunitySettingsChatPage from "./pages/community_settings/communitySettingsChatPage"; import CommunitySettingsDescriptionPage from "./pages/community_settings/communitySettingsDescriptionPage"; import CommunitySettingsLinksPage from "./pages/community_settings/communitySettingsLinksPage"; +import CommunityWithChatSettingsDescriptionEditPage from "./pages/community_settings/communityWithChatSettingsDescriptionEditPage"; +import CommunityWithoutChatSettingsDescriptionEditPage from "./pages/community_settings/communityWithoutChatSettingsDescriptionEditPage"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -92,6 +94,17 @@ function App() { path="/community/:communityId/settings/description" element={} /> + + } + /> + + } + /> + } diff --git a/client/src/components/chatPreview.component.tsx b/client/src/components/chatPreview.component.tsx index 9a07887..fd684a5 100644 --- a/client/src/components/chatPreview.component.tsx +++ b/client/src/components/chatPreview.component.tsx @@ -60,7 +60,11 @@ const ChatPreviewComponent = ( } >
    -

    {props.chatData.name}

    +

    + {props.chatData.name.length > 20 + ? props.chatData.name.slice(0, 20) + "..." + : props.chatData.name} +

    {props.chatData.membersCount} участников

    @@ -96,7 +100,11 @@ const ChatPreviewComponent = ( className={`flex flex-col gap-[2px]`} onClick={props.onClick} > -

    {props.chatData.name}

    +

    + {props.chatData.name.length > 20 + ? props.chatData.name.slice(0, 20) + "..." + : props.chatData.name} +

    {props.chatData.membersCount} участников

    @@ -134,7 +142,11 @@ const ChatPreviewComponent = ( className={`flex flex-col gap-[2px]`} onClick={props.onClick} > -

    {props.chatData.name}

    +

    + {props.chatData.name.length > 20 + ? props.chatData.name.slice(0, 20) + "..." + : props.chatData.name} +

    {props.chatData.membersCount} участников

    diff --git a/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx b/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx index f0b4a57..557bd33 100644 --- a/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx +++ b/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx @@ -1,7 +1,65 @@ import EBBComponent from "../../components/enableBackButtonComponent"; +import InfoBlockComponent from "../../components/infoBlock.component"; +import InfoParagraphComponent from "../../components/infoParagraph.component"; +import { useNavigate, useParams } from "react-router-dom"; +import useCommunity from "../../hooks/communities/fetchHooks/useСommunity"; +import useUserStore from "../../stores/user.store"; +import FixedBottomButtonComponent from "../../components/fixedBottomButton.component"; const CommunitySettingsDescriptionPage = () => { - return Settings Description Page; + const { communityId } = useParams(); + const { data: communityData, isPending } = useCommunity(communityId!); + const { userData } = useUserStore(); + const navigate = useNavigate(); + + if (isPending || !communityData) return null; + + const currentMember = communityData.members?.find( + (member) => member.user.id === userData.id + ); + const isAdmin = currentMember?.isAdmin === true; + const chatId = communityData.tgChatID; + const goToEditDescription = () => { + if (isAdmin) { + if (chatId) { + navigate( + `/community/${communityId}/settings/description/edit/with_chat` + ); + } else { + navigate( + `/community/${communityId}/settings/description/edit/without_chat` + ); + } + } + }; + + return ( + +
    +
    + + + + + + +
    +
    + {isAdmin && ( + + )} +
    + ); }; export default CommunitySettingsDescriptionPage; diff --git a/client/src/pages/community_settings/communityWithChatSettingsDescriptionEditPage.tsx b/client/src/pages/community_settings/communityWithChatSettingsDescriptionEditPage.tsx new file mode 100644 index 0000000..aad2c2e --- /dev/null +++ b/client/src/pages/community_settings/communityWithChatSettingsDescriptionEditPage.tsx @@ -0,0 +1,73 @@ +import { useNavigate, useParams } from "react-router-dom"; +import EBBComponent from "../../components/enableBackButtonComponent"; +import FixedBottomButtonComponent from "../../components/fixedBottomButton.component"; +import TextareaFieldComponent from "../../components/form/textareaField.component"; +import useCommunity from "../../hooks/communities/fetchHooks/useСommunity"; +import { useState } from "react"; +import usePatchCommunity from "../../hooks/communities/mutations/usePatchCommunity"; + +const CommunityWithChatSettingsDescriptionEditPage = () => { + const navigate = useNavigate(); + const { communityId } = useParams(); + + const patchCommunityMutation = usePatchCommunity(); + + const { data: communityData, isPending } = useCommunity(communityId!); + const [description, setDescription] = useState( + communityData?.description! + ); + + if (isPending) return null; + + const handleClick = async () => { + try { + await patchCommunityMutation.mutateAsync({ + communityData: { + avatar: communityData?.avatar!, + config: communityData?.config!, + description: description, + id: communityData?.id!, + name: communityData?.name!, + tgChatID: communityData?.tgChatID!, + }, + }); + navigate(-1); + } catch (error) { + alert("Произошла ошибка при обновлении сообщества"); + console.error("Ошибка при патче сообщества:", error); + } + }; + return ( + +
    +
    +

    + Основная информация +

    +
    + + + +
    + +
    +
    +
    +
    + ); +}; + +export default CommunityWithChatSettingsDescriptionEditPage; diff --git a/client/src/pages/community_settings/communityWithoutChatSettingsDescriptionEditPage.tsx b/client/src/pages/community_settings/communityWithoutChatSettingsDescriptionEditPage.tsx new file mode 100644 index 0000000..d250ba6 --- /dev/null +++ b/client/src/pages/community_settings/communityWithoutChatSettingsDescriptionEditPage.tsx @@ -0,0 +1,7 @@ +import EBBComponent from "../../components/enableBackButtonComponent"; + +const CommunityWithoutChatSettingsDescriptionEditPage = () => { + return Settings without chat; +}; + +export default CommunityWithoutChatSettingsDescriptionEditPage; From eae25e9a34d17f2999683019435457e3ecb013d9 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Sun, 18 May 2025 20:21:41 +0300 Subject: [PATCH 54/78] added all new functionality with multiprofiles --- client/src/App.tsx | 4 + .../src/components/chatPreview.component.tsx | 52 ++++-- client/src/pages/community.page.tsx | 3 - client/src/pages/communityLinks.page.tsx | 4 - client/src/pages/communityQR.page.tsx | 0 .../communitySettingsChatPage.tsx | 102 ++++++++++- .../communitySettingsDescriptionPage.tsx | 59 +++++-- .../communitySettingsLinksPage.tsx | 2 +- .../communitySettingsProfilePage.tsx | 6 +- ...ityWithChatSettingsDescriptionEditPage.tsx | 4 +- ...WithoutChatSettingsDescriptionEditPage.tsx | 163 +++++++++++++++++- .../postApiTypes/patchCommunity.interface.ts | 2 +- 12 files changed, 346 insertions(+), 55 deletions(-) delete mode 100644 client/src/pages/communityLinks.page.tsx delete mode 100644 client/src/pages/communityQR.page.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index f3fb0b2..4b96d40 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -34,6 +34,7 @@ import CommunitySettingsDescriptionPage from "./pages/community_settings/communi import CommunitySettingsLinksPage from "./pages/community_settings/communitySettingsLinksPage"; import CommunityWithChatSettingsDescriptionEditPage from "./pages/community_settings/communityWithChatSettingsDescriptionEditPage"; import CommunityWithoutChatSettingsDescriptionEditPage from "./pages/community_settings/communityWithoutChatSettingsDescriptionEditPage"; +import useInitDataStore from "./stores/InitData.store"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -69,6 +70,9 @@ function App() { } }, [location.pathname]); + const { initData } = useInitDataStore(); + console.log(initData); + const routes = ( }> diff --git a/client/src/components/chatPreview.component.tsx b/client/src/components/chatPreview.component.tsx index fd684a5..26e8bec 100644 --- a/client/src/components/chatPreview.component.tsx +++ b/client/src/components/chatPreview.component.tsx @@ -45,31 +45,45 @@ const ChatPreviewComponent = ( props.className } > -
    - +
    0 + ? "chat-content flex items-center gap-[7px] w-full transition-transform duration-300 mb-[16px]" + : "chat-content flex items-center gap-[7px] w-full transition-transform duration-300" } + onClick={props.onClick} > -
    -

    - {props.chatData.name.length > 20 - ? props.chatData.name.slice(0, 20) + "..." - : props.chatData.name} -

    -

    - {props.chatData.membersCount} участников -

    + +
    +
    +

    + {props.chatData.name.length > 20 + ? props.chatData.name.slice(0, 20) + "..." + : props.chatData.name} +

    +

    + {props.chatData.membersCount} участников +

    +
    + {props.chatData.description.length > 0 && ( + <> +
    +

    + {props.chatData.description ?? ""} +

    + + )}
    ); diff --git a/client/src/pages/community.page.tsx b/client/src/pages/community.page.tsx index 057d662..2a15a80 100644 --- a/client/src/pages/community.page.tsx +++ b/client/src/pages/community.page.tsx @@ -83,9 +83,6 @@ const CommunityPage = () => { index={0} onClick={() => goToCommunitySettings()} /> -
    - goToMyProfile()} /> -
    )} diff --git a/client/src/pages/communityLinks.page.tsx b/client/src/pages/communityLinks.page.tsx deleted file mode 100644 index b3f07d4..0000000 --- a/client/src/pages/communityLinks.page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// import StarsBuka from "../assets/star_buka.png"; -// const CommunityLinksPage = () => { -// return; -// }; diff --git a/client/src/pages/communityQR.page.tsx b/client/src/pages/communityQR.page.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/client/src/pages/community_settings/communitySettingsChatPage.tsx b/client/src/pages/community_settings/communitySettingsChatPage.tsx index 64ada8b..3cced64 100644 --- a/client/src/pages/community_settings/communitySettingsChatPage.tsx +++ b/client/src/pages/community_settings/communitySettingsChatPage.tsx @@ -1,7 +1,105 @@ +import { openTelegramLink } from "@telegram-apps/sdk-react"; +import CopyFieldComponent from "../../components/copyField.component"; import EBBComponent from "../../components/enableBackButtonComponent"; - +import FixedBottomButtonComponent from "../../components/fixedBottomButton.component"; +import StepComponent from "../../components/step.component"; +import { BOT_USERNAME } from "../../shared/constants"; +import { useNavigate, useParams } from "react-router-dom"; +import useCommunity from "../../hooks/communities/fetchHooks/useСommunity"; +import Plus from "../../assets/plus_white.svg"; +import StarByak from "../../assets/star_buka.png"; const CommunitySettingsChatPage = () => { - return Community; + const navigate = useNavigate(); + const { communityId } = useParams(); + const { data: communityData, isPending } = useCommunity(communityId!); + + if (isPending) return null; + + if (communityData?.tgChatID !== 0 && communityData?.tgChatID) { + return ( + +
    +
    +

    Привязка к чату

    +
    +
    +
    + +
    +
    + +
    +

    + Сообщество уже привязано к чату! +

    +
    +
    +
    +
    + ); + } + + return ( + +
    +
    +

    Привязка к чату

    +
    + +
    +
    + + + +
    + +
    + +
    { + openTelegramLink(`https://t.me/${BOT_USERNAME}?startgroup=`); + }} + > +

    Добавить в чат

    + +
    +
    + +
    + +
    +
    + 1 +
    +
    + * привязать сообщество к чату +
    можно только один раз +
    +
    +
    +
    +
    + + { + navigate(-1); + }} + /> +
    +
    + ); }; export default CommunitySettingsChatPage; diff --git a/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx b/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx index 557bd33..3022dcd 100644 --- a/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx +++ b/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx @@ -5,14 +5,21 @@ import { useNavigate, useParams } from "react-router-dom"; import useCommunity from "../../hooks/communities/fetchHooks/useСommunity"; import useUserStore from "../../stores/user.store"; import FixedBottomButtonComponent from "../../components/fixedBottomButton.component"; - +import { openTelegramLink } from "@telegram-apps/sdk-react"; +import ButtonComponent from "../../components/button.component"; +import { handleImageError } from "../../utils/imageErrorHandler"; +import TGWhite from "../../assets/tg_white.svg"; +import DevImage from "../../assets/dev.png"; +import useCommunityPreview from "../../hooks/communities/fetchHooks/useCommunityPreview"; const CommunitySettingsDescriptionPage = () => { const { communityId } = useParams(); const { data: communityData, isPending } = useCommunity(communityId!); + const { data: communityPreviewData, isPending: isPreviewPending } = + useCommunityPreview(communityId!); const { userData } = useUserStore(); const navigate = useNavigate(); - if (isPending || !communityData) return null; + if (isPending || !communityData || isPreviewPending) return null; const currentMember = communityData.members?.find( (member) => member.user.id === userData.id @@ -21,7 +28,8 @@ const CommunitySettingsDescriptionPage = () => { const chatId = communityData.tgChatID; const goToEditDescription = () => { if (isAdmin) { - if (chatId) { + if (chatId && chatId !== 0) { + console.log("chatId"); navigate( `/community/${communityId}/settings/description/edit/with_chat` ); @@ -36,28 +44,41 @@ const CommunitySettingsDescriptionPage = () => { return (
    -
    - - - +
    + + +
    +

    + {communityData.name.length > 25 + ? communityData.name.slice(0, 25) + "..." + : communityData.name} +

    +

    + Участников: {communityData.members.length} +

    +
    +
    +
    + + {isAdmin && ( + + )}
    - {isAdmin && ( - - )} ); }; diff --git a/client/src/pages/community_settings/communitySettingsLinksPage.tsx b/client/src/pages/community_settings/communitySettingsLinksPage.tsx index f834247..4dd09f9 100644 --- a/client/src/pages/community_settings/communitySettingsLinksPage.tsx +++ b/client/src/pages/community_settings/communitySettingsLinksPage.tsx @@ -55,7 +55,7 @@ const CommunitySettingsLinksPage = () => {
    )} { if (isQrShowing) { diff --git a/client/src/pages/community_settings/communitySettingsProfilePage.tsx b/client/src/pages/community_settings/communitySettingsProfilePage.tsx index 8f0e9da..a27d687 100644 --- a/client/src/pages/community_settings/communitySettingsProfilePage.tsx +++ b/client/src/pages/community_settings/communitySettingsProfilePage.tsx @@ -52,7 +52,7 @@ const CommunitySettingsProfilePage = () => { description: description!, name: name!, id: communityId!, - tgChatID: tgChatId!, + tgChatID: tgChatId, }, }); @@ -173,8 +173,8 @@ const CommunitySettingsProfilePage = () => { handleContinue()} />
    diff --git a/client/src/pages/community_settings/communityWithChatSettingsDescriptionEditPage.tsx b/client/src/pages/community_settings/communityWithChatSettingsDescriptionEditPage.tsx index aad2c2e..ff4bdf4 100644 --- a/client/src/pages/community_settings/communityWithChatSettingsDescriptionEditPage.tsx +++ b/client/src/pages/community_settings/communityWithChatSettingsDescriptionEditPage.tsx @@ -28,7 +28,7 @@ const CommunityWithChatSettingsDescriptionEditPage = () => { description: description, id: communityData?.id!, name: communityData?.name!, - tgChatID: communityData?.tgChatID!, + tgChatID: communityData?.tgChatID, }, }); navigate(-1); @@ -57,7 +57,7 @@ const CommunityWithChatSettingsDescriptionEditPage = () => {
    { - return Settings without chat; + const patchCommunityMutation = usePatchCommunity(); + const setCommunityAvatarMutation = useSetCommunityAvatar(); + + const navigate = useNavigate(); + const { communityId } = useParams(); + const { data: communityData } = useCommunity(communityId!); + + const [avatar, setAvatar] = useState(null); + + const [description, setDescription] = useState(null); + const [name, setName] = useState(null); + + const [previewUrl, setPreviewUrl] = useState( + avatar ? URL.createObjectURL(avatar) : "" + ); + + const [errorMessage, setErrorMessage] = useState(null); + + const inputRef = useRef(null); + + const handleUploadClick = () => { + setErrorMessage(null); + if (inputRef.current) { + inputRef.current.value = ""; + inputRef.current.click(); + } + }; + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + if (!ALLOWED_IMAGE_TYPES.includes(file.type)) { + setErrorMessage("Допустимы PNG или JPG"); + return; + } + + if (file.size > MAX_FILE_SIZE) { + setErrorMessage("Размер файла не должен превышать 5 МБ"); + return; + } + setAvatar(file); + setPreviewUrl(URL.createObjectURL(file)); + } + }; + + const handleClick = async () => { + try { + const response = await patchCommunityMutation.mutateAsync({ + communityData: { + avatar: communityData?.avatar!, + config: communityData?.config!, + description: description!, + id: communityId!, + name: name!, + }, + }); + + if (response) { + await setCommunityAvatarMutation.mutateAsync({ + communityId: communityId!, + avatar: avatar!, + }); + console.log("updatedAvatar", avatar); + } + navigate(-1); + + console.log("updatedAvatar", avatar); + } catch (error) { + alert("Произошла ошибка при обновлении сообщества"); + console.error("Ошибка при патче сообщества:", error); + } + }; + + return ( + +
    +
    +
    +

    + Основная информация +

    +
    + + + + + +
    +

    + Аватар сообщества +

    + +
    +
    + {avatar !== null && previewUrl !== null && ( +
    + avatar +
    + )} + +
    + Upload icon +

    загрузить с устройства

    +
    + +
    + {errorMessage && ( +
    + {errorMessage} +
    + )} +
    +
    +
    + +
    + await handleClick()} + /> +
    +
    +
    +
    + ); }; export default CommunityWithoutChatSettingsDescriptionEditPage; diff --git a/client/src/types/postApiTypes/patchCommunity.interface.ts b/client/src/types/postApiTypes/patchCommunity.interface.ts index cc9e096..9ab14f1 100644 --- a/client/src/types/postApiTypes/patchCommunity.interface.ts +++ b/client/src/types/postApiTypes/patchCommunity.interface.ts @@ -6,5 +6,5 @@ export interface PatchCommunity { description: string; id: string; name: string; - tgChatID: number; + tgChatID?: number; } From 36d0b5225a7495cc15ef74b4ee3b51d27542eba4 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Sun, 18 May 2025 20:23:41 +0300 Subject: [PATCH 55/78] fix lint --- client/src/App.tsx | 4 ---- client/src/pages/community.page.tsx | 5 ----- .../communitySettingsDescriptionPage.tsx | 7 +------ .../communityWithoutChatSettingsDescriptionEditPage.tsx | 3 --- client/src/pages/editProfileCommunity.page.tsx | 2 -- client/src/pages/initial.page.tsx | 3 +-- 6 files changed, 2 insertions(+), 22 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 4b96d40..f3fb0b2 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -34,7 +34,6 @@ import CommunitySettingsDescriptionPage from "./pages/community_settings/communi import CommunitySettingsLinksPage from "./pages/community_settings/communitySettingsLinksPage"; import CommunityWithChatSettingsDescriptionEditPage from "./pages/community_settings/communityWithChatSettingsDescriptionEditPage"; import CommunityWithoutChatSettingsDescriptionEditPage from "./pages/community_settings/communityWithoutChatSettingsDescriptionEditPage"; -import useInitDataStore from "./stores/InitData.store"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -70,9 +69,6 @@ function App() { } }, [location.pathname]); - const { initData } = useInitDataStore(); - console.log(initData); - const routes = ( }> diff --git a/client/src/pages/community.page.tsx b/client/src/pages/community.page.tsx index 2a15a80..de9468c 100644 --- a/client/src/pages/community.page.tsx +++ b/client/src/pages/community.page.tsx @@ -10,7 +10,6 @@ import { motion } from "motion/react"; import NotFound from "../assets/notFound.svg"; import useSearchMembers from "../hooks/members/search/useSearchMembers"; import useCommunity from "../hooks/communities/fetchHooks/useСommunity"; -import ProfileComponent from "../components/profile.component"; const containerVariants = { visible: { transition: { @@ -36,10 +35,6 @@ const CommunityPage = () => { navigate(`/community/${communityId}/member/${memberId}`); }; - const goToMyProfile = () => { - navigate(`/profile/current/${communityId}`); - }; - const goToCommunitySettings = () => { navigate(`/community/${communityId}/settings`); }; diff --git a/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx b/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx index 3022dcd..eb5c471 100644 --- a/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx +++ b/client/src/pages/community_settings/communitySettingsDescriptionPage.tsx @@ -5,17 +5,13 @@ import { useNavigate, useParams } from "react-router-dom"; import useCommunity from "../../hooks/communities/fetchHooks/useСommunity"; import useUserStore from "../../stores/user.store"; import FixedBottomButtonComponent from "../../components/fixedBottomButton.component"; -import { openTelegramLink } from "@telegram-apps/sdk-react"; -import ButtonComponent from "../../components/button.component"; import { handleImageError } from "../../utils/imageErrorHandler"; -import TGWhite from "../../assets/tg_white.svg"; import DevImage from "../../assets/dev.png"; import useCommunityPreview from "../../hooks/communities/fetchHooks/useCommunityPreview"; const CommunitySettingsDescriptionPage = () => { const { communityId } = useParams(); const { data: communityData, isPending } = useCommunity(communityId!); - const { data: communityPreviewData, isPending: isPreviewPending } = - useCommunityPreview(communityId!); + const { isPending: isPreviewPending } = useCommunityPreview(communityId!); const { userData } = useUserStore(); const navigate = useNavigate(); @@ -29,7 +25,6 @@ const CommunitySettingsDescriptionPage = () => { const goToEditDescription = () => { if (isAdmin) { if (chatId && chatId !== 0) { - console.log("chatId"); navigate( `/community/${communityId}/settings/description/edit/with_chat` ); diff --git a/client/src/pages/community_settings/communityWithoutChatSettingsDescriptionEditPage.tsx b/client/src/pages/community_settings/communityWithoutChatSettingsDescriptionEditPage.tsx index 4abada7..ba92268 100644 --- a/client/src/pages/community_settings/communityWithoutChatSettingsDescriptionEditPage.tsx +++ b/client/src/pages/community_settings/communityWithoutChatSettingsDescriptionEditPage.tsx @@ -73,11 +73,8 @@ const CommunityWithoutChatSettingsDescriptionEditPage = () => { communityId: communityId!, avatar: avatar!, }); - console.log("updatedAvatar", avatar); } navigate(-1); - - console.log("updatedAvatar", avatar); } catch (error) { alert("Произошла ошибка при обновлении сообщества"); console.error("Ошибка при патче сообщества:", error); diff --git a/client/src/pages/editProfileCommunity.page.tsx b/client/src/pages/editProfileCommunity.page.tsx index a2ab05d..49fb54f 100644 --- a/client/src/pages/editProfileCommunity.page.tsx +++ b/client/src/pages/editProfileCommunity.page.tsx @@ -14,12 +14,10 @@ import { MemberConfig } from "../types/member/memberConfig.interface"; import { FieldValue } from "../types/fields/fieldValue.interface"; import { FieldType } from "../types/fields/field.type"; import useCommunity from "../hooks/communities/fetchHooks/useСommunity"; -import useInitDataStore from "../stores/InitData.store"; import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; const EditProfileCommunityPage = () => { const navigate = useNavigate(); - const { initData } = useInitDataStore(); const { communityId } = useParams(); const { data: userData, isSuccess } = useGetMe(); const { data: communityData } = useCommunity(communityId ?? ""); diff --git a/client/src/pages/initial.page.tsx b/client/src/pages/initial.page.tsx index 53fb004..09b12a0 100644 --- a/client/src/pages/initial.page.tsx +++ b/client/src/pages/initial.page.tsx @@ -3,9 +3,8 @@ import { useNavigate } from "react-router-dom"; import useInitDataStore from "../stores/InitData.store"; const InitialPage = () => { - const { initDataStartParam: startParam, initData } = useInitDataStore(); + const { initDataStartParam: startParam } = useInitDataStore(); const navigate = useNavigate(); - console.log(initData); useEffect(() => { if ( startParam !== undefined && From 871e73f810a80e00f10b75d5ffa5423b3710ef02 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Sun, 18 May 2025 21:02:20 +0300 Subject: [PATCH 56/78] about flow changes --- client/src/components/about.component.tsx | 20 +++++++ client/src/pages/about.page.tsx | 68 +++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/client/src/components/about.component.tsx b/client/src/components/about.component.tsx index d7cd81d..48d6720 100644 --- a/client/src/components/about.component.tsx +++ b/client/src/components/about.component.tsx @@ -3,6 +3,9 @@ interface aboutComponentProps { imageSrc: string; contentText: string; buttonText: string; + politics?: boolean; + cancelButtonText?: string; + handleCancelButtonClick?: () => void; } const AboutComponent = (props: aboutComponentProps) => { @@ -19,9 +22,26 @@ const AboutComponent = (props: aboutComponentProps) => { {props.contentText}

    + + {props.politics && ( +
    + выбирая вариант «Принимаю»,вы соглашаетесь с положениями + + политики конфиденциальности + +
    + )}
    + {props.politics && ( + + )}
    -
    - {props.politics && ( +
    + {props.cancelButtonText && ( )}
    { setIsQrShowing(true); diff --git a/client/src/utils/protectedRoute.tsx b/client/src/utils/protectedRoute.tsx index 236654d..4368176 100644 --- a/client/src/utils/protectedRoute.tsx +++ b/client/src/utils/protectedRoute.tsx @@ -5,7 +5,7 @@ import useUserStore from "../stores/user.store"; const ProtectedRoute = () => { const userStore = useUserStore(); - const { isPending, data, isSuccess, isError } = useGetMe(); + const { isPending, data, isSuccess, isLoading } = useGetMe(); useEffect(() => { if (isSuccess && data) { @@ -13,13 +13,10 @@ const ProtectedRoute = () => { } }, [isSuccess, data]); - if (isPending) return null; + if (isPending || isLoading) return null; if (data?.user.id) return ; else return ; - - // if (!data?.user) return - // else (isSuccess) return ; }; export default ProtectedRoute; From 0cbc2109b249dea821b18f8ea7e90c0c0bbca081 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Mon, 19 May 2025 01:59:01 +0300 Subject: [PATCH 59/78] qr boilerplate --- .../communitySettingsLinksPage.tsx | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/client/src/pages/community_settings/communitySettingsLinksPage.tsx b/client/src/pages/community_settings/communitySettingsLinksPage.tsx index 4dd09f9..9b3fccc 100644 --- a/client/src/pages/community_settings/communitySettingsLinksPage.tsx +++ b/client/src/pages/community_settings/communitySettingsLinksPage.tsx @@ -3,15 +3,32 @@ import StarByak from "../../assets/star_buka.png"; import CopyFieldComponent from "../../components/copyField.component"; import { BOT_USERNAME } from "../../shared/constants"; import FixedBottomButtonComponent from "../../components/fixedBottomButton.component"; -import { useState } from "react"; +import { useRef, useState } from "react"; import useCommunity from "../../hooks/communities/fetchHooks/useСommunity"; import { QRCodeSVG } from "qrcode.react"; +import { downloadFile } from "@telegram-apps/sdk-react"; const CommunitySettingsLinksPage = () => { const { communityId } = useParams(); const { data: communityData } = useCommunity(communityId!); const [isQrShowing, setIsQrShowing] = useState(false); const navigate = useNavigate(); + const qrRef = useRef(null); + + const downloadQRCode = async () => { + const svg = qrRef.current; + if (!svg) return; + + const svgData = new XMLSerializer().serializeToString(svg); + const blob = new Blob([svgData], { type: "image/svg+xml" }); + const url = URL.createObjectURL(blob); + const name = `${communityData?.name || "qr-code"}.svg`; + console.log(url); + if (downloadFile.isAvailable()) { + await downloadFile(url, name); + } + }; + return (
    {!isQrShowing && ( @@ -38,6 +55,20 @@ const CommunitySettingsLinksPage = () => { > QRRR
    +
    + {" "} + +
    +
    )} {isQrShowing && ( From f90dac10caf332d2bac48ca4be410a976047ad98 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Mon, 19 May 2025 03:14:33 +0300 Subject: [PATCH 60/78] done in creation community --- client/package-lock.json | 60 ---------------- client/package.json | 2 - client/src/assets/download_black.svg | 3 + client/src/assets/qr.svg | 14 ++++ .../communitySettingsLinksPage.tsx | 60 +++++++--------- .../create_community/communityLinksPage.tsx | 70 ++++++++----------- 6 files changed, 72 insertions(+), 137 deletions(-) create mode 100644 client/src/assets/download_black.svg create mode 100644 client/src/assets/qr.svg diff --git a/client/package-lock.json b/client/package-lock.json index 27409d3..d41092c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,10 +15,8 @@ "@tanstack/react-query": "^5.74.8", "@telegram-apps/sdk-react": "^3.1.2", "axios": "^1.8.3", - "html2canvas": "^1.4.1", "motion": "^12.5.0", "node": "^23.9.0", - "qrcode.react": "^4.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.56.3", @@ -2173,15 +2171,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -2420,15 +2409,6 @@ "node": ">= 8" } }, - "node_modules/css-line-break": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", - "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "license": "MIT", - "dependencies": { - "utrie": "^1.0.2" - } - }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -3174,19 +3154,6 @@ "node": ">= 0.4" } }, - "node_modules/html2canvas": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", - "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "license": "MIT", - "dependencies": { - "css-line-break": "^2.1.0", - "text-segmentation": "^1.0.3" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4007,15 +3974,6 @@ "node": ">=6" } }, - "node_modules/qrcode.react": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", - "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", - "license": "ISC", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4366,15 +4324,6 @@ "node": ">=6" } }, - "node_modules/text-segmentation": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", - "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "license": "MIT", - "dependencies": { - "utrie": "^1.0.2" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4516,15 +4465,6 @@ "punycode": "^2.1.0" } }, - "node_modules/utrie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", - "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "license": "MIT", - "dependencies": { - "base64-arraybuffer": "^1.0.2" - } - }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", diff --git a/client/package.json b/client/package.json index 97e7388..e223f47 100644 --- a/client/package.json +++ b/client/package.json @@ -18,10 +18,8 @@ "@tanstack/react-query": "^5.74.8", "@telegram-apps/sdk-react": "^3.1.2", "axios": "^1.8.3", - "html2canvas": "^1.4.1", "motion": "^12.5.0", "node": "^23.9.0", - "qrcode.react": "^4.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.56.3", diff --git a/client/src/assets/download_black.svg b/client/src/assets/download_black.svg new file mode 100644 index 0000000..2fcac8d --- /dev/null +++ b/client/src/assets/download_black.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/qr.svg b/client/src/assets/qr.svg new file mode 100644 index 0000000..9eadddb --- /dev/null +++ b/client/src/assets/qr.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/client/src/pages/community_settings/communitySettingsLinksPage.tsx b/client/src/pages/community_settings/communitySettingsLinksPage.tsx index 9b3fccc..cb8a4f7 100644 --- a/client/src/pages/community_settings/communitySettingsLinksPage.tsx +++ b/client/src/pages/community_settings/communitySettingsLinksPage.tsx @@ -3,29 +3,25 @@ import StarByak from "../../assets/star_buka.png"; import CopyFieldComponent from "../../components/copyField.component"; import { BOT_USERNAME } from "../../shared/constants"; import FixedBottomButtonComponent from "../../components/fixedBottomButton.component"; -import { useRef, useState } from "react"; +import { useState } from "react"; import useCommunity from "../../hooks/communities/fetchHooks/useСommunity"; -import { QRCodeSVG } from "qrcode.react"; import { downloadFile } from "@telegram-apps/sdk-react"; +import QrIcon from "../../assets/qr.svg"; +import DownloadIcon from "../../assets/download_black.svg"; const CommunitySettingsLinksPage = () => { const { communityId } = useParams(); const { data: communityData } = useCommunity(communityId!); const [isQrShowing, setIsQrShowing] = useState(false); const navigate = useNavigate(); - const qrRef = useRef(null); - const downloadQRCode = async () => { - const svg = qrRef.current; - if (!svg) return; + const filename = `${communityData?.name || "qr-code"}.png`; - const svgData = new XMLSerializer().serializeToString(svg); - const blob = new Blob([svgData], { type: "image/svg+xml" }); - const url = URL.createObjectURL(blob); - const name = `${communityData?.name || "qr-code"}.svg`; - console.log(url); if (downloadFile.isAvailable()) { - await downloadFile(url, name); + await downloadFile( + `https://api.qrserver.com/v1/create-qr-code/?data=https://t.me/${BOT_USERNAME}/app?startapp=${communityId}&size=150x150`, + filename + ); } }; @@ -47,28 +43,20 @@ const CommunitySettingsLinksPage = () => { link />
    - -
    { - setIsQrShowing(true); - }} - > - QRRR -
    -
    - {" "} - +
    + +

    Скачать qr-код

    +
    + setIsQrShowing(true)} + src={QrIcon} + className=" bg-white p-[10.5px] object-fit rounded-[14px]" />
    -
    )} {isQrShowing && ( @@ -79,9 +67,11 @@ const CommunitySettingsLinksPage = () => { qr-код для присоединения

    -
    )} diff --git a/client/src/pages/create_community/communityLinksPage.tsx b/client/src/pages/create_community/communityLinksPage.tsx index 89e036e..15524f2 100644 --- a/client/src/pages/create_community/communityLinksPage.tsx +++ b/client/src/pages/create_community/communityLinksPage.tsx @@ -3,32 +3,26 @@ import StarByak from "../../assets/star_buka.png"; import CopyFieldComponent from "../../components/copyField.component"; import { BOT_USERNAME } from "../../shared/constants"; import FixedBottomButtonComponent from "../../components/fixedBottomButton.component"; -import { useRef, useState } from "react"; +import { useState } from "react"; import useCommunity from "../../hooks/communities/fetchHooks/useСommunity"; -import { QRCodeSVG } from "qrcode.react"; +import { downloadFile } from "@telegram-apps/sdk-react"; +import QrIcon from "../../assets/qr.svg"; +import DownloadIcon from "../../assets/download_black.svg"; const CommunityLinksPage = () => { const { communityId } = useParams(); const { data: communityData } = useCommunity(communityId!); const [isQrShowing, setIsQrShowing] = useState(false); const navigate = useNavigate(); - const qrRef = useRef(null); + const downloadQRCode = async () => { + const filename = `${communityData?.name || "qr-code"}.png`; - const downloadQRCode = () => { - const svg = qrRef.current; - if (!svg) return; - - const svgData = new XMLSerializer().serializeToString(svg); - const blob = new Blob([svgData], { type: "image/svg+xml" }); - const url = URL.createObjectURL(blob); - - const link = document.createElement("a"); - link.href = url; - link.download = `${communityData?.name || "qr-code"}.svg`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); + if (downloadFile.isAvailable()) { + await downloadFile( + `https://api.qrserver.com/v1/create-qr-code/?data=https://t.me/${BOT_USERNAME}/app?startapp=${communityId}&size=150x150`, + filename + ); + } }; return ( @@ -52,27 +46,21 @@ const CommunityLinksPage = () => { link />
    -
    - {" "} - +
    + +

    Скачать qr-код

    +
    + setIsQrShowing(true)} + src={QrIcon} + className=" bg-white p-[10.5px] object-fit rounded-[14px]" />
    - -
    { - setIsQrShowing(true); - }} - > - QRRR -
    )} {isQrShowing && ( @@ -83,9 +71,11 @@ const CommunityLinksPage = () => { qr-код для присоединения

    -
    )} From 0751d12ea6b91a85e0fe2515021bd6bc230c5328 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Mon, 19 May 2025 04:19:41 +0300 Subject: [PATCH 61/78] fix animations --- .../components/chatMember.card.component.tsx | 4 +- client/src/pages/community.page.tsx | 65 +++++++++---------- .../communitySettingsLinksPage.tsx | 1 + .../create_community/communityLinksPage.tsx | 1 + 4 files changed, 33 insertions(+), 38 deletions(-) diff --git a/client/src/components/chatMember.card.component.tsx b/client/src/components/chatMember.card.component.tsx index b532868..031e969 100644 --- a/client/src/components/chatMember.card.component.tsx +++ b/client/src/components/chatMember.card.component.tsx @@ -7,11 +7,11 @@ import { useExtractFields } from "../hooks/utils/extractFields"; const cardVariants = { hidden: { opacity: 0, y: 20 }, - visible: (index: number) => ({ + visible: () => ({ opacity: 1, y: 0, transition: { - delay: index > 10 ? index * 0.01 : 11 * 0.01, + delay: 11 * 0.01, duration: 0.4, ease: "easeOut", }, diff --git a/client/src/pages/community.page.tsx b/client/src/pages/community.page.tsx index de9468c..0249128 100644 --- a/client/src/pages/community.page.tsx +++ b/client/src/pages/community.page.tsx @@ -39,7 +39,11 @@ const CommunityPage = () => { navigate(`/community/${communityId}/settings`); }; - const { data: previewChatData, isSuccess } = useCommunity(communityId ?? ""); + const { + data: previewChatData, + isSuccess, + isPending: isCommunityPending, + } = useCommunity(communityId ?? ""); const scrollContainerRef = useRef(null); const handleScroll = () => { @@ -61,6 +65,7 @@ const CommunityPage = () => { return () => clearTimeout(timer); }, [isPending]); + if (isPending || isCommunityPending) return null; return ( @@ -87,41 +92,29 @@ const CommunityPage = () => { placeholder="Поиск участников" className="mt-[20px]" /> - {loadedFirstTime && ( -
      - {chatData?.map((user, index) => ( - goToProfile(user.user.id)} - index={index} - key={user.user.id} - member={user} - /> - ))} -
    - )} - {!loadedFirstTime && ( - - {chatData?.map((user, index) => ( - goToProfile(user.user.id)} - index={index} - key={user.user.id} - member={user} - onAnimationComplete={ - index === chatData.length - 1 - ? () => setLoadedFirstTime(true) - : undefined - } - /> - ))} - - )} + + + {chatData?.map((user, index) => ( + goToProfile(user.user.id)} + index={index} + key={index} + member={user} + onAnimationComplete={ + index === chatData.length - 1 + ? () => setLoadedFirstTime(true) + : undefined + } + /> + ))} + + {!chatData && !isPending && (
    diff --git a/client/src/pages/community_settings/communitySettingsLinksPage.tsx b/client/src/pages/community_settings/communitySettingsLinksPage.tsx index cb8a4f7..d527d0e 100644 --- a/client/src/pages/community_settings/communitySettingsLinksPage.tsx +++ b/client/src/pages/community_settings/communitySettingsLinksPage.tsx @@ -75,6 +75,7 @@ const CommunitySettingsLinksPage = () => { />
    )} +
    { />
    )} +
    Date: Mon, 19 May 2025 04:28:26 +0300 Subject: [PATCH 62/78] fix lint --- client/src/pages/community.page.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/client/src/pages/community.page.tsx b/client/src/pages/community.page.tsx index 0249128..c83e88c 100644 --- a/client/src/pages/community.page.tsx +++ b/client/src/pages/community.page.tsx @@ -3,7 +3,7 @@ import EBBComponent from "../components/enableBackButtonComponent"; import RequireMembershipComponent from "../components/requireMembership.component"; import SearchBarComponent from "../components/searchBar.component"; import ChatPreviewComponent from "../components/chatPreview.component"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; import useChatSearchStore from "../stores/chatSearch.store"; import ChatMemberCardComponent from "../components/chatMember.card.component"; import { motion } from "motion/react"; @@ -21,7 +21,6 @@ const CommunityPage = () => { const navigate = useNavigate(); const { communityId } = useParams(); - const [loadedFirstTime, setLoadedFirstTime] = useState(false); const { getScroll, saveScroll, getSearchQuery, saveSearchQuery } = useChatSearchStore(); @@ -106,11 +105,6 @@ const CommunityPage = () => { index={index} key={index} member={user} - onAnimationComplete={ - index === chatData.length - 1 - ? () => setLoadedFirstTime(true) - : undefined - } /> ))} From 10d4f6469d3875b9c8743e82eb1d2c34da01996d Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Mon, 19 May 2025 11:48:02 +0300 Subject: [PATCH 63/78] fixed membership requiring --- client/src/components/requireMembership.component.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/client/src/components/requireMembership.component.tsx b/client/src/components/requireMembership.component.tsx index d455434..92dca25 100644 --- a/client/src/components/requireMembership.component.tsx +++ b/client/src/components/requireMembership.component.tsx @@ -13,17 +13,15 @@ const RequireMembershipComponent = (props: RequireMembershipComponentProps) => { const { initDataStartParam } = useInitDataStore(); const chatID = props.chatID ?? initDataStartParam; - const { isSuccess, isPending, data, isRefetching } = useCommunityPreview( - chatID || "" - ); + const { isSuccess, data, isFetching } = useCommunityPreview(chatID || ""); useEffect(() => { - if (!isRefetching && isSuccess && !data?.isMember) { + if (!isFetching && isSuccess && !data?.isMember) { navigate("/invite", { replace: true }); } }, [isSuccess, data]); - if (isPending || (data && !data.isMember)) { + if (isFetching || (data && !data.isMember)) { return null; } From ea9a95139f87507f799782aa0f51bc1d0a151915 Mon Sep 17 00:00:00 2001 From: PriestFaria Date: Mon, 19 May 2025 12:43:15 +0300 Subject: [PATCH 64/78] preloader --- client/src/api/users.api.ts | 8 ++++---- client/src/components/about.component.tsx | 2 +- client/src/components/loader.component.tsx | 12 ++++++++++++ client/src/hooks/users/mutations/useCreateMe.ts | 9 +++++++-- client/src/index.css | 1 - client/src/pages/about.page.tsx | 2 ++ client/src/pages/community.page.tsx | 3 ++- .../communitySettingsChatPage.tsx | 3 ++- .../communitySettingsDescriptionPage.tsx | 3 ++- .../community_settings/communitySettingsPage.tsx | 3 ++- .../communitySettingsProfilePage.tsx | 3 ++- ...munityWithChatSettingsDescriptionEditPage.tsx | 3 ++- client/src/pages/registration.page.tsx | 3 ++- client/src/utils/protectedRoute.tsx | 16 +++++++++++++--- 14 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 client/src/components/loader.component.tsx diff --git a/client/src/api/users.api.ts b/client/src/api/users.api.ts index 28d02be..b972f24 100644 --- a/client/src/api/users.api.ts +++ b/client/src/api/users.api.ts @@ -11,7 +11,7 @@ export const getMe = async (initData: string): Promise => { return response.data; } catch (error) { console.error("Ошибка при получении профиля текущего пользователя:", error); - return null; + throw error; } }; @@ -26,7 +26,7 @@ export const updateMe = async ( return response.data; } catch (error) { console.error("Ошибка при изменении профиля", error); - return null; + throw error; } }; @@ -44,8 +44,8 @@ export const createMe = async (initData: string): Promise => { return response.data; } catch (error) { console.error("Ошибка при содании профиля", error); + throw error; } - return null; }; export const deleteMe = async (initData: string): Promise => { @@ -58,6 +58,6 @@ export const deleteMe = async (initData: string): Promise => { return response.data; } catch (error) { console.error("Ошибка при удалении профиля", error); + throw error; } - return null; }; diff --git a/client/src/components/about.component.tsx b/client/src/components/about.component.tsx index bd5b819..ff21708 100644 --- a/client/src/components/about.component.tsx +++ b/client/src/components/about.component.tsx @@ -47,7 +47,7 @@ const AboutComponent = (props: aboutComponentProps) => { )}