diff --git a/client/package-lock.json b/client/package-lock.json index e855fe7..d41092c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,16 +8,22 @@ "name": "client", "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", "@telegram-apps/sdk-react": "^3.1.2", "axios": "^1.8.3", "motion": "^12.5.0", "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", + "uuid": "^11.1.0", "zustand": "^5.0.3" }, "devDependencies": { @@ -332,6 +338,73 @@ "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/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", + "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", @@ -1511,6 +1584,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", @@ -3917,6 +4016,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", @@ -4350,6 +4465,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 e935da6..e223f47 100644 --- a/client/package.json +++ b/client/package.json @@ -11,16 +11,22 @@ "preview": "vite preview" }, "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", "@telegram-apps/sdk-react": "^3.1.2", "axios": "^1.8.3", "motion": "^12.5.0", "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", + "uuid": "^11.1.0", "zustand": "^5.0.3" }, "devDependencies": { 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 ab71084..08c6269 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,20 +1,45 @@ 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 { getMePreview } from "./api/api"; -import { backButton, initData } from "@telegram-apps/sdk-react"; -import useUserStore from "./stores/user.store"; -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 { useCallback, useEffect, useMemo } from "react"; +import { backButton } from "@telegram-apps/sdk-react"; 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 { + AboutDeclinePolicy, + AboutFirstPage, + AboutSecondPage, + AboutThirdPage, +} from "./pages/about.page"; +import ChatPage from "./pages/community.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 RegistrationPage from "./pages/registration.page"; +import InvitationPage from "./pages/invitation.page"; +import EditProfileCommunityPage from "./pages/editProfileCommunity.page"; +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"; +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"; +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"; +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"; +import CommunityWithChatSettingsDescriptionEditPage from "./pages/community_settings/communityWithChatSettingsDescriptionEditPage"; +import CommunityWithoutChatSettingsDescriptionEditPage from "./pages/community_settings/communityWithoutChatSettingsDescriptionEditPage"; +import PolicyPage from "./pages/policy.page"; const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -25,18 +50,15 @@ 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 { resetStore: resetWithChatStore } = useCommunityWithChatInfoStore(); + const { resetStore: resetWithoutChatStore } = + useCommunityWithoutChatInfoStore(); + const disableAnimation = useMemo( + () => + location.pathname.startsWith("/community/create/with_chat") || + location.pathname.startsWith("/community/create/without_chat"), + [location.pathname] + ); const setBackButtonHandler = useCallback(() => { backButton.onClick(() => { @@ -44,41 +66,138 @@ function App() { }); }, []); + useEffect(() => setBackButtonHandler(), [setBackButtonHandler]); + useEffect(() => { - checkAuth(); - setBackButtonHandler(); - }, []); + if (!location.pathname.startsWith("/community/create")) { + resetWithChatStore(); + resetWithoutChatStore(); + } + }, [location.pathname]); + + const routes = ( + + }> + } /> + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + + } + /> + + } + /> + + } + /> + }> + } /> + }> + } + /> + } /> + } + /> + + } + > + } + /> + + } + /> + + + + + + + } + /> + + } + /> + } + /> + } /> + } + /> + + + } /> + } /> + } /> + } /> + } /> + } /> + + ); return ( -
- - - - }> - } /> - } /> - } /> - } /> - } /> - } /> - - } /> - } /> - } /> - } /> - - - -
+ +
+ {disableAnimation ? ( + routes + ) : ( + + + {routes} + + + )} +
+
); } diff --git a/client/src/api/api.ts b/client/src/api/api.ts index b4142d1..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,227 +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 -): 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 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 -): 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..2fecb62 --- /dev/null +++ b/client/src/api/communities.api.ts @@ -0,0 +1,228 @@ +import { AvatarCommunity } from "../types/community/avatar.community.interface"; +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"; + +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 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 +): 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, + memberConfig: MemberConfig +): Promise => { + try { + await api.post(`/communities/id/${id}/join`, memberConfig, { + headers: { "X-Api-Token": initData }, + }); + return null; + } catch (error) { + console.error("Ошибка при присоединении к сообществу:", error); + return null; + } +}; + +export const leaveCommunity = async ( + initData: string, + id: string +): Promise => { + try { + await api.post( + `/communities/id/${id}/leave`, + {}, + { + headers: { "X-Api-Token": initData }, + } + ); + return null; + } catch (error) { + console.error("Ошибка при выходе из сообщества:", error); + return null; + } +}; + +export const patchMember = async ( + initData: string, + communityId: string, + memberId: string, + data: PatchMember +): Promise => { + try { + await api.patch( + `/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, + 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/id/${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..b972f24 --- /dev/null +++ b/client/src/api/users.api.ts @@ -0,0 +1,63 @@ +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); + throw error; + } +}; + +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); + throw error; + } +}; + +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); + throw error; + } +}; + +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); + throw error; + } +}; diff --git a/client/src/assets/about.png b/client/src/assets/about.png deleted file mode 100644 index 7b0699a..0000000 Binary files a/client/src/assets/about.png and /dev/null differ diff --git a/client/src/assets/about_decline.png b/client/src/assets/about_decline.png new file mode 100644 index 0000000..4d266e4 Binary files /dev/null and b/client/src/assets/about_decline.png differ diff --git a/client/src/assets/about_third.png b/client/src/assets/about_third.png new file mode 100644 index 0000000..41941ce Binary files /dev/null and b/client/src/assets/about_third.png differ 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/community_info.svg b/client/src/assets/community_info.svg new file mode 100644 index 0000000..9cf1790 --- /dev/null +++ b/client/src/assets/community_info.svg @@ -0,0 +1,5 @@ + + + + + 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/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/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/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/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/assets/star_buka.png b/client/src/assets/star_buka.png new file mode 100644 index 0000000..8d5efe2 Binary files /dev/null and b/client/src/assets/star_buka.png differ 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 0000000..376fb12 Binary files /dev/null and b/client/src/assets/telegram_avatars_cluster_1.png differ 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 0000000..599b7aa Binary files /dev/null and b/client/src/assets/telegram_avatars_cluster_2.png differ 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/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/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/about.component.tsx b/client/src/components/about.component.tsx index b53e3b1..ff21708 100644 --- a/client/src/components/about.component.tsx +++ b/client/src/components/about.component.tsx @@ -3,6 +3,10 @@ interface aboutComponentProps { imageSrc: string; contentText: string; buttonText: string; + politics?: boolean; + cancelButtonText?: string; + handleCancelButtonClick?: () => void; + onPolicyClick?: () => void; } const AboutComponent = (props: aboutComponentProps) => { @@ -14,16 +18,36 @@ const AboutComponent = (props: aboutComponentProps) => { About -
+

{props.contentText}

+ + {props.politics && ( +
+ выбирая вариант «Принимаю»,вы соглашаетесь с положениями{" "} + + политики конфиденциальности + +
+ )}
-
+
+ {props.cancelButtonText && ( + + )} diff --git a/client/src/pages/policy.page.tsx b/client/src/pages/policy.page.tsx new file mode 100644 index 0000000..e3b6448 --- /dev/null +++ b/client/src/pages/policy.page.tsx @@ -0,0 +1,226 @@ +import { useNavigate } from "react-router-dom"; +import EBBComponent from "../components/enableBackButtonComponent"; +import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; + +const PolicyPage = () => { + const navigate = useNavigate(); + return ( + +
+

+ Политика конфиденциальности +

+ {/*

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Sed animi + quos maiores asperiores voluptatum, accusamus saepe itaque nisi dolor + est sunt facilis hic accusantium id voluptatibus necessitatibus esse + neque, praesentium nam ut, deleniti officia eveniet vero. + Perspiciatis, dignissimos veritatis excepturi harum aperiam nobis + molestiae iusto dolore reprehenderit repellat possimus fugit, + explicabo numquam, magni repudiandae nesciunt aliquid nemo enim amet + atque iste sint? Optio officia velit non aliquid maiores animi. + Deleniti numquam accusamus accusantium vitae unde earum nulla. Ipsam + quasi iusto exercitationem unde voluptas ullam, vel saepe inventore + hic minima, consectetur aut totam facilis eligendi architecto + accusantium fugiat quia labore cumque mollitia nam obcaecati assumenda + excepturi atque. Culpa, aliquid? Nesciunt, nihil ipsam incidunt + provident quaerat impedit vitae dolore dicta laborum nisi? Libero in + suscipit ipsum pariatur aut consequuntur ipsam, cumque velit quam + corporis, molestias quasi hic quibusdam minima eius maiores vitae + veniam corrupti, repellat beatae? Quia nihil sapiente animi eum + commodi assumenda voluptatem dignissimos, facere, obcaecati officiis + expedita distinctio voluptates. Ipsa doloribus esse id est nulla + facere impedit amet nam dolore deleniti expedita eveniet, voluptatibus + voluptatum officiis eligendi odio cum. Obcaecati aut dolorum ex. Sit + laborum quisquam nulla aliquid suscipit quas repudiandae, dicta harum + cum iusto autem necessitatibus delectus ad, corporis molestias. + Mollitia sunt laboriosam recusandae exercitationem, ex libero odit + vitae cumque maxime, autem cum minus totam commodi esse dignissimos! + Asperiores necessitatibus deleniti minima est blanditiis natus + expedita debitis excepturi ipsum, harum saepe qui quos, vitae + molestias, quisquam voluptatum nobis quidem possimus neque. Quas sequi + atque debitis accusamus enim necessitatibus dolor praesentium? In, + commodi! Debitis iure sint, eveniet est culpa temporibus sapiente vero + dolorum ducimus harum quas accusantium tempora odio mollitia + consequatur vitae adipisci laborum cumque, possimus iusto! Tempora + molestiae fuga voluptatem ullam fugit quod quam minus, cupiditate + culpa assumenda repudiandae minima nisi eius eum quae? Optio debitis + molestias accusantium porro praesentium quos error delectus quae + laboriosam dolor commodi ex, laborum dicta aspernatur cum tenetur + animi fugiat quo mollitia totam quibusdam. Voluptates dolore ducimus + excepturi et eveniet cupiditate cum aspernatur veniam aliquam + similique adipisci quam, alias minima fugiat minus sint ipsam, est + dolorem neque incidunt? Sunt sit corporis officia dolorem quasi, omnis + tempora. Quos ullam deleniti modi magnam nemo voluptate dolorum nulla + ratione impedit possimus maiores odit ipsa eveniet illo ut, ad, et + quibusdam quia quidem dignissimos rerum consequatur, quo consectetur + accusamus. Illo ipsum doloremque placeat sunt rem. Maiores, ut + facilis, excepturi cum eligendi dignissimos ea porro sunt quia dicta + voluptatem optio eius assumenda rem tempore. Minus earum veniam amet + aut magnam possimus, quam, maiores enim quisquam nulla molestias + blanditiis consequatur natus aliquid aperiam tempora quos, quae facere + id. Distinctio obcaecati quisquam et, placeat dicta soluta modi + repellat repudiandae. A eveniet consequatur totam magni tempora minus + officiis, voluptatibus, molestiae dicta deleniti ut eum minima aliquam + quo vero culpa, distinctio ratione sunt omnis dolor ea sit nobis + deserunt! Earum unde veniam explicabo minus eligendi maxime quia + repellendus rerum illo, temporibus placeat vero in sunt id, corrupti + recusandae officia minima aspernatur facilis corporis deleniti + repudiandae sit qui! Earum exercitationem incidunt doloremque nemo + optio cumque, dolores deserunt odit numquam modi corrupti ipsum odio, + molestiae tempora reprehenderit veniam facilis quis id soluta maiores + dolor aut temporibus ratione. Quas incidunt necessitatibus corrupti + voluptas similique labore veritatis ipsam deserunt, ullam sed. Porro + numquam illum similique accusantium quidem sapiente nam corporis + quisquam, nihil fuga doloremque temporibus. Laborum ex unde odit aut + itaque quis quibusdam quos illum iste eos maxime harum necessitatibus + minima voluptas explicabo, maiores, tempora consequuntur provident + voluptatum a modi, voluptatem eveniet accusamus aliquid. Voluptates, + saepe qui consequatur ut omnis vero earum enim, nulla soluta veniam + eos praesentium, repellendus totam laudantium cumque ipsa quam nam + quidem nostrum mollitia eaque quas. Officiis sed quasi blanditiis + earum in est aut rem sit dolore eius. Quisquam atque amet magnam + incidunt doloremque ullam suscipit quo laudantium odit quidem dolorum + eligendi natus adipisci, aut, officia assumenda dolorem alias tenetur. + Repellendus, debitis quaerat! Fugiat sed hic eligendi vel cumque harum + obcaecati voluptates veritatis, quo exercitationem praesentium veniam + numquam at nobis dolorem explicabo tempora quisquam ipsam totam ipsum + iure nostrum ratione. Odio, aspernatur reprehenderit incidunt + inventore perspiciatis modi commodi placeat tenetur magnam sed, animi + omnis reiciendis? Repudiandae sapiente quisquam minus nesciunt ab + maxime. Nam corrupti eligendi laborum. Doloremque eius obcaecati iste + architecto maiores. Odit voluptatem vero, nulla alias, mollitia, totam + quisquam nesciunt modi ut optio qui iure cumque necessitatibus atque! + Eaque odio aperiam, exercitationem itaque sapiente impedit ea + cupiditate mollitia dolores quaerat reiciendis nobis quae numquam + voluptatem, fugit dolorem maiores? Amet nisi vero, officia, placeat + cum modi fugiat nemo quae asperiores dolorum corrupti dolor ab + voluptatem recusandae unde. Voluptates in rem asperiores magni + voluptatum! Ab quo minus, porro enim dolores iure similique saepe est + illo voluptates optio natus blanditiis vitae corrupti sed aut + perspiciatis? Voluptas dolorum cum aliquam, praesentium ducimus, + commodi debitis nemo molestias recusandae officia a voluptates nisi et + hic provident voluptatem laboriosam! Cumque, molestiae! Possimus + veritatis quidem velit odit hic, tempore et molestias nemo iusto ipsam + blanditiis, asperiores optio rem delectus cupiditate, repellat + accusamus voluptatem eaque molestiae quam impedit nostrum non aut + similique? Eveniet odio, voluptates mollitia dolorum facilis est + quidem aliquid aspernatur voluptatum temporibus maiores eaque nesciunt + ducimus illo assumenda sit! Maxime, ipsam nisi eligendi sapiente ut + dicta adipisci quibusdam, eum cum quidem molestias assumenda + praesentium debitis laborum aut? Accusantium accusamus totam alias + incidunt molestias, porro, praesentium explicabo repellendus ipsum + libero facilis deleniti nam dignissimos, ratione quisquam quod unde! + Quas perspiciatis error corrupti, illo delectus nisi esse, incidunt + dolorum doloribus recusandae quos inventore. Sed itaque molestias ipsa + quae. Eligendi modi, ullam quisquam magnam ut debitis repellat + voluptate sunt architecto sequi fugit consequatur dolores asperiores + maiores similique quos laborum labore? Ab atque culpa laudantium + reprehenderit distinctio eaque rem saepe veritatis cupiditate fugit + dignissimos quis asperiores sed eligendi libero veniam magni unde + suscipit illo molestiae accusamus architecto, amet iusto? Soluta + facilis, eius sapiente sed perspiciatis, a, aperiam dignissimos earum + consectetur odit deserunt quibusdam nihil nostrum sequi qui. Magnam + consequatur dolore unde possimus tempore atque animi? Alias animi + laboriosam doloremque totam ducimus debitis voluptatum porro adipisci, + minima vel magnam cumque in blanditiis odit exercitationem! +

*/} +

+

+ 1. Общие положения +

+ Настоящее Соглашение (далее – «Соглашение») регулирует порядок + обработки и защиты персональных данных пользователей (далее – + «Пользователь»), предоставляемых Обществу с ограниченной + ответственностью «Шампстех» (ОГРН: 1247800098820, ИНН: 7842225368, + юридический адрес: 191014, Г.САНКТ-ПЕТЕРБУРГ, ВН.ТЕР.Г. МУНИЦИПАЛЬНЫЙ + ОКРУГ СМОЛЬНИНСКОЕ, УЛ ВОССТАНИЯ, Д. 43, ЛИТЕРА А, КВ. 15, далее – + «Оператор»). +

+ 2. Согласие на обработку персональных данных +

+ Передавая свои персональные данные, Пользователь дает согласие + Оператору на их обработку, включая сбор, систематизацию, накопление, + хранение, уточнение (обновление, изменение), использование, + обезличивание, блокирование, удаление и уничтожение персональных + данных. +

+ 3. Перечень персональных данных +

+ Персональные данные, на обработку которых дается согласие, могут + включать, но не ограничиваться следующей информацией: +
+ – Фамилия, имя, отчество; +
+ – Контактный номер телефона; +
+ – Иные данные, предоставленные Пользователем при взаимодействии с + Оператором. +
+

+ 4. Цели обработки персональных данных +

+ Оператор обрабатывает персональные данные для следующих целей: – + Предоставление доступа к функционалу сайта/сервиса; +
+ – Консультирование и обратная связь; +
+ – Улучшение качества сервиса и продуктов Оператора. +
+

+ 5. Условия обработки данных +

+ – Обработка персональных данных осуществляется в соответствии с + Федеральным законом РФ №152-ФЗ «О персональных данных». +
+ – Оператор принимает необходимые правовые, организационные и + технические меры для защиты персональных данных. +
+ – Доступ к персональным данным имеют только уполномоченные сотрудники + Оператора. +
+

+ 6. Срок хранения персональных данных +

+ Персональные данные хранятся в течение срока, необходимого для + достижения целей обработки, либо до момента отзыва согласия + Пользователя. +
+

+ 7. Отзыв согласия +

+ Пользователь имеет право в любой момент отозвать согласие на обработку + персональных данных, направив письменное уведомление на электронную + почту: team@shamps.dev, либо по почтовому адресу Оператора. +

+ 8. Передача персональных данных третьим лицам +

+ Оператор не передает персональные данные третьим лицам, за исключением + случаев, предусмотренных законодательством РФ либо с согласия + Пользователя. +

+ 9. Изменение условий Соглашения +

+ Оператор оставляет за собой право вносить изменения в настоящее + Соглашение. Новая редакция вступает в силу с момента ее размещения на + сайте, если иное не предусмотрено новой редакцией. +

+ 10. Контактная информация +

+ ООО «Шампстех» Юридический адрес: 191014, Г.САНКТ-ПЕТЕРБУРГ, ВН.ТЕР.Г. + МУНИЦИПАЛЬНЫЙ ОКРУГ СМОЛЬНИНСКОЕ, УЛ ВОССТАНИЯ, Д. 43, ЛИТЕРА А, КВ. + 15 Email: team@shamps.dev +

+
+ +
+ navigate(-1)} + state={"active"} + /> +
+ ); +}; + +export default PolicyPage; diff --git a/client/src/pages/profile.page.tsx b/client/src/pages/profile.page.tsx index 9ca867e..601fce4 100644 --- a/client/src/pages/profile.page.tsx +++ b/client/src/pages/profile.page.tsx @@ -1,28 +1,25 @@ -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 useMember from "../hooks/members/fetchHooks/useMember"; +import useCommunity from "../hooks/communities/fetchHooks/useСommunity"; + const ProfilePage = () => { - const { userId } = useParams(); - const [userData, setUserData] = useState(); - const fetchProfileData = async () => { - const data = await getUserById(initData.raw() ?? "", userId ?? ""); - if (data) { - setUserData(data); - } - }; + const { communityId, memberId } = useParams(); + const { data, isLoading } = useMember(communityId ?? "", memberId ?? ""); + const { data: communityData } = useCommunity(communityId!); + + if (isLoading || !data) return null; - useEffect(() => { - fetchProfileData(); - }, []); + const userData = data.user; + const fieldsData = data.config.fields; + const orderedFieldsPattern = communityData?.config.fields; return ( @@ -55,25 +52,20 @@ const ProfilePage = () => { />
- - - - -
- ПОДРОБНЕЕ -
- - - + {orderedFieldsPattern && orderedFieldsPattern?.length > 0 && ( + + {orderedFieldsPattern && + orderedFieldsPattern.map((field, index) => { + return ( + + ); + })} + + )}
diff --git a/client/src/pages/registration.page.tsx b/client/src/pages/registration.page.tsx index 0f63abe..a55afeb 100644 --- a/client/src/pages/registration.page.tsx +++ b/client/src/pages/registration.page.tsx @@ -1,213 +1,122 @@ -import { useState, useEffect, useCallback } from "react"; -import InputFieldComponent from "../components/inputField.component"; -import TextareaFieldComponent from "../components/textareaField.component"; -import { UserData } from "../types/user.interface"; +import useCommunityPreview from "../hooks/communities/fetchHooks/useCommunityPreview"; +import useInitDataStore from "../stores/InitData.store"; +import useGetMe from "../hooks/users/fetchHooks/useGetMe"; 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 { useState } from "react"; +import { fieldsToFieldValues } from "../mappers/FieldValues"; +import { MemberConfig } from "../types/member/memberConfig.interface"; +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"; +import FixedBottomButtonComponent from "../components/fixedBottomButton.component"; +import Loader from "../components/loader.component"; const RegistrationPage = () => { - const launchParams = retrieveLaunchParams(); + const { initDataStartParam: communityId } = useInitDataStore(); + const { data: communityData, isPending } = useCommunityPreview( + communityId ?? "" + ); + const { data: userData, isLoading } = useGetMe(); - const startParam = initDataStartParam(); - const userStore = useUserStore(); - const navigate = useNavigate(); - const avatar = initDataUser()?.photo_url; - 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() !== ""; + if (isPending || isLoading) return ; - return isFilled; - }; + const orderedFieldsPattern = communityData?.config.fields; + const baseValues = fieldsToFieldValues(orderedFieldsPattern!); + const [fields, setFields] = useState>(baseValues); - const MAX_INPUT_LENGTH = 40; - const MAX_TEXTAREA_LENGTH = 230; + const [isChanged, setIsChanged] = useState(false); - const handleInputChange = (field: keyof UserData, value: string) => { - setProfileData((prevState) => ({ - ...prevState, - [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 handleFieldChange = (title: string, value: string, type: FieldType) => { + const updatedFields: Record = structuredClone(fields); + updatedFields[title][type]!.value = value; + setIsChanged(!fieldsAreEqual(updatedFields, baseValues)); + setFields(updatedFields); }; - const registerUser = async () => { - const response = await createMe(initData.raw() ?? "", profileData); - if (response) { - checkAuth(); - navigate("/chats", { replace: true }); + const joinCommunityMutation = useJoinCommunity(); + const navigate = useNavigate(); + const handleSubmit = async () => { + if (isChanged) { + const config: MemberConfig = { + fields: fields, + }; - if (startParam !== undefined && startParam.length > 0) { - const joinResponse = await joinMe(initData.raw() ?? "", startParam); - if (joinResponse) { - navigate(`/chat/${startParam}`); - } - } + await joinCommunityMutation.mutateAsync({ + communityId: communityId ?? "", + memberConfig: config, + }); + navigate(`/communities`, { replace: true }); + navigate(`/community/${communityId}`); } }; - const fetchBio = useCallback(async () => { - const data = await getMePreview(initData.raw() ?? ""); - if (data) { - setProfileData((prevState) => ({ - ...prevState, - bio: data.bio, - })); - } - }, []); - useEffect(() => { - fetchBio(); - }, []); - 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) registerUser(); - }} - className="flex w-full justify-center pt-[20px]" - > - { - if (isFilled) registerUser(); - }} - state={isFilled ? "active" : "disabled"} - /> -
- )} +
+ +
+

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

+

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

+
+
{ + e.preventDefault(); + }} + > + {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) + } + /> + ); + } + })} - {launchParams.tgWebAppPlatform !== "ios" && ( -
{ - if (isFilled) registerUser(); - }} - state={isFilled ? "active" : "disabled"} + content="Готово" + state={isChanged ? "active" : "disabled"} + handleClick={() => handleSubmit()} /> -
- )} - + +
+
); }; + export default RegistrationPage; diff --git a/client/src/shared/constants.ts b/client/src/shared/constants.ts index 97d53c6..68f7a1f 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 * 1024; //bytes +export const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg"]; 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/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; diff --git a/client/src/stores/create_community/communityWithChatInfo.store.ts b/client/src/stores/create_community/communityWithChatInfo.store.ts new file mode 100644 index 0000000..9930863 --- /dev/null +++ b/client/src/stores/create_community/communityWithChatInfo.store.ts @@ -0,0 +1,39 @@ +import { create } from "zustand"; +import { Field } from "../../types/fields/field.interface"; + +interface CommunityInfoStore { + description: string; + fields: Field[]; + chatRequired: boolean; + communityId: string; + + setDescription: (v: string) => void; + setFields: (fields: Field[]) => void; + setChatRequired: (v: boolean) => void; + setCommunityId: (communityId: string) => void; + + resetStore: () => void; +} + +const useCommunityWithChatInfoStore = create((set) => ({ + description: "", + fields: [], + chatRequired: false, + chatId: "", + communityId: "", + + setDescription: (v) => set({ description: v }), + setFields: (fields) => set({ fields }), + setChatRequired: (v) => set({ chatRequired: v }), + setCommunityId: (communityId: string) => set({ communityId: communityId }), + + resetStore: () => + set({ + description: "", + fields: [], + chatRequired: false, + communityId: "", + }), +})); + +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/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/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/types/community/community.interface.ts b/client/src/types/community/community.interface.ts new file mode 100644 index 0000000..99a64cd --- /dev/null +++ b/client/src/types/community/community.interface.ts @@ -0,0 +1,14 @@ +import { Member } from "../member/member.interface"; +import { CommunityConfig } from "./communityConfig.interface"; + +export interface Community { + avatar: string; + config: CommunityConfig; + isMember: boolean; + 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..2805258 --- /dev/null +++ b/client/src/types/fields/field.type.ts @@ -0,0 +1,2 @@ +export type FieldType = "textinput" | "textarea"; +export const AllFieldTypes: 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/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/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/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/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..9ab14f1 --- /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..31d884f --- /dev/null +++ b/client/src/types/user/user.interface.ts @@ -0,0 +1,8 @@ +export interface User { + avatar: string; + firstName: string; + lastName: 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; +} 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; 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/equalFields.ts b/client/src/utils/equalFields.ts new file mode 100644 index 0000000..509587b --- /dev/null +++ b/client/src/utils/equalFields.ts @@ -0,0 +1,21 @@ +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; diff --git a/client/src/utils/fieldsNotEmpty.ts b/client/src/utils/fieldsNotEmpty.ts new file mode 100644 index 0000000..f741fbf --- /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) => { + 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; 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 new file mode 100644 index 0000000..7b048f7 --- /dev/null +++ b/client/src/utils/protectedRoute.tsx @@ -0,0 +1,21 @@ +import { Navigate, Outlet } from "react-router-dom"; +import { useEffect } from "react"; +import useGetMe from "../hooks/users/fetchHooks/useGetMe"; +import useUserStore from "../stores/user.store"; +import Loader from "../components/loader.component"; + +const ProtectedRoute = () => { + const userStore = useUserStore(); + const { isPending, isLoading, isSuccess, data } = useGetMe(); + useEffect(() => { + if (isSuccess && data) { + userStore.setUserData(data.user); + } + }, [isSuccess, data]); + + if (isPending || isLoading) return ; + else if (data?.user?.id || isSuccess) return ; + else return ; +}; + +export default ProtectedRoute;