diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index bdf59aa0..d6c22b04 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -11,6 +11,7 @@ export default tseslint.config( '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/consistent-type-definitions': 'off', '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-extraneous-class': 'off', }, } ) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dfaf2978..e76a7344 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { TanStackProvider, ThemeContext } from '@context' +import { ThemeContext } from '@context' import '@styles/index.scss' import { TonConnectUIProvider } from '@tonconnect/ui-react' import { checkIsMobile, checkStartAppParams } from '@utils' @@ -6,7 +6,9 @@ import { useContext, useEffect } from 'react' import { useNavigate, useParams } from 'react-router-dom' import config from '@config' +import { AuthService } from '@services' import { useUser, useUserActions } from '@store' +import { useAuthQuery } from '@store-new' import Routes from './Routes' @@ -17,6 +19,8 @@ function App() { const { darkTheme } = useContext(ThemeContext) const navigate = useNavigate() + // useAuthQuery() + const { authenticateUserAction, fetchUserAction } = useUserActions() const { isAuthenticated } = useUser() @@ -90,19 +94,17 @@ function App() { } }, [isAuthenticated]) - if (!isAuthenticated) return null + // if (!AuthService.isAuth()) return null return ( - - - {Routes} - - + + {Routes} + ) } diff --git a/frontend/src/common/hooks/useClipboard.ts b/frontend/src/common/hooks/useClipboard.ts index 6c8add19..82010efc 100644 --- a/frontend/src/common/hooks/useClipboard.ts +++ b/frontend/src/common/hooks/useClipboard.ts @@ -3,11 +3,11 @@ import { useToast } from '@components' export function useClipboard() { const { showToast } = useToast() - const webApp = window.Telegram.WebApp + const webApp = window.Telegram?.WebApp return { copy: (text: string, message: string) => { - webApp.HapticFeedback.notificationOccurred('success') + webApp?.HapticFeedback.notificationOccurred('success') navigator.clipboard .writeText(text.toString()) .then(() => { diff --git a/frontend/src/common/utils/constants.ts b/frontend/src/common/utils/constants.ts index 8d2c31fe..300e4900 100644 --- a/frontend/src/common/utils/constants.ts +++ b/frontend/src/common/utils/constants.ts @@ -1,16 +1,20 @@ -import { ChatsPopularSortBy } from '@types' +import { ChatsPopularOrderBy } from '@types' export const API_VALIDATION_ERROR = 'Fill fields correctly' export const API_ERRORS = {} export const TANSTACK_KEYS = { - CHATS_POPULAR: (sortBy: ChatsPopularSortBy) => ['chats', 'popular', sortBy], + AUTH: ['auth'], + USER: ['user'], + CHATS_POPULAR: (sortBy: ChatsPopularOrderBy) => ['chats', 'popular', sortBy], CHAT: (slug: string) => ['chat', slug], ADMIN_CHATS: ['admin', 'chats'], } export const TANSTACK_TTL = { + AUTH: 5 * 60 * 1000, // 5 minute + USER: 1 * 60 * 1000, // 1 minute CHATS_POPULAR: 5 * 60 * 1000, // 5 minute ADMIN_CHATS: 5 * 60 * 1000, // 5 minute CHAT: 5 * 60 * 1000, // 5 minute diff --git a/frontend/src/common/utils/goTo.ts b/frontend/src/common/utils/goTo.ts index b866d04a..d0d3e620 100644 --- a/frontend/src/common/utils/goTo.ts +++ b/frontend/src/common/utils/goTo.ts @@ -1,9 +1,9 @@ export const goTo = (link: string) => { - const webApp = window.Telegram.WebApp + const webApp = window.Telegram?.WebApp if (link.includes('t.me')) { - webApp.openTelegramLink(link) + webApp?.openTelegramLink(link) } else { - webApp.openLink(link) + webApp?.openLink(link) } } diff --git a/frontend/src/common/utils/index.ts b/frontend/src/common/utils/index.ts index 0a86dfcb..bfa9869a 100644 --- a/frontend/src/common/utils/index.ts +++ b/frontend/src/common/utils/index.ts @@ -11,3 +11,4 @@ export * from './createMembersCount' export * from './createConditionDescription' export * from './checkIsMobile' export * from './hapticFeedback' +export * from './pluralize' diff --git a/frontend/src/common/utils/pluralize.ts b/frontend/src/common/utils/pluralize.ts new file mode 100644 index 00000000..e6076e85 --- /dev/null +++ b/frontend/src/common/utils/pluralize.ts @@ -0,0 +1,17 @@ +export const pluralize = ( + textForms: [string, string, string], // [one, few, many] + multiplier: number // number of items +): string => { + const mod10 = multiplier % 10 + const mod100 = multiplier % 100 + + const prettifiedMultiplier = multiplier.toLocaleString() + + if (mod10 === 1 && mod100 !== 11) { + return `${prettifiedMultiplier} ${textForms[0]}` + } + if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) { + return `${prettifiedMultiplier} ${textForms[1]}` + } + return `${prettifiedMultiplier} ${textForms[2]}` +} diff --git a/frontend/src/components/BlockNew/BlockNew.tsx b/frontend/src/components/BlockNew/BlockNew.tsx index e54278c7..fa724e44 100644 --- a/frontend/src/components/BlockNew/BlockNew.tsx +++ b/frontend/src/components/BlockNew/BlockNew.tsx @@ -3,6 +3,7 @@ import cn from 'classnames' import styles from './BlockNew.module.scss' interface BlockNewProps { + id?: string children: React.ReactNode margin?: string defaultWidth?: boolean @@ -50,6 +51,7 @@ interface BlockNewProps { } export const BlockNew = ({ + id, children, fixed, row, @@ -65,6 +67,7 @@ export const BlockNew = ({ }: BlockNewProps) => { return (
{ - if (webApp.MainButton && onClick) { - webApp.MainButton.onClick(onClick) + if (webApp?.MainButton && onClick) { + webApp?.MainButton.onClick(onClick) return () => { - if (webApp.MainButton) { - webApp.MainButton.offClick(onClick) + if (webApp?.MainButton) { + webApp?.MainButton.offClick(onClick) } } } }, [onClick]) if ( - webApp.platform === 'unknown' && + webApp?.platform === 'unknown' && process.env.NODE_ENV !== 'production' && isVisible ) { diff --git a/frontend/src/components/Toast/ToastElement.tsx b/frontend/src/components/Toast/ToastElement.tsx index d4add6e5..d8242bd5 100644 --- a/frontend/src/components/Toast/ToastElement.tsx +++ b/frontend/src/components/Toast/ToastElement.tsx @@ -17,7 +17,7 @@ export interface ToastOptions { | 'warning' } -const webApp = window.Telegram.WebApp +const webApp = window.Telegram?.WebApp export const ToastElement = ({ children, @@ -52,11 +52,11 @@ export const ToastElement = ({ useEffect(() => { setTimeout(() => { if (type === 'error') { - webApp.HapticFeedback.notificationOccurred('error') + webApp?.HapticFeedback.notificationOccurred('error') } if (type === 'success') { - webApp.HapticFeedback.notificationOccurred('success') + webApp?.HapticFeedback.notificationOccurred('success') } setIsOpen(true) }, 50) diff --git a/frontend/src/context/TanStackProvider/TanStackProvider.tsx b/frontend/src/context/TanStackProvider/TanStackProvider.tsx index 57900ab9..112b40d6 100644 --- a/frontend/src/context/TanStackProvider/TanStackProvider.tsx +++ b/frontend/src/context/TanStackProvider/TanStackProvider.tsx @@ -2,9 +2,11 @@ import { useToast } from '@components' import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' import { QueryClient, QueryCache, MutationCache } from '@tanstack/react-query' import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' -import { API_ERRORS } from '@utils' +import { API_ERRORS, TANSTACK_KEYS } from '@utils' import { PropsWithChildren, useMemo } from 'react' +import { AuthService } from '@services' + const HIDDEN_ERRORS = ['user_forbidden', 'synced_recently', 'TON_CONNECT'] export const TanStackProvider = (props: PropsWithChildren) => { @@ -53,10 +55,19 @@ export const TanStackProvider = (props: PropsWithChildren) => { retryOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, + enabled: (query) => { + if (query.queryKey === TANSTACK_KEYS.AUTH) return true + return AuthService.isAuth() + }, }, mutations: { retry: false, throwOnError: false, + onMutate: () => { + if (!AuthService.isAuth()) { + throw new Error('Authorization required') + } + }, }, }, }), diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 50f14c2f..387b56a5 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,5 +1,5 @@ import { ToastProvider } from '@components' -import { ThemeProvider } from '@context' +import { TanStackProvider, ThemeProvider } from '@context' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' @@ -8,12 +8,14 @@ import App from './App' createRoot(document.getElementById('root') as HTMLElement).render( - - - - - - - + + + + + + + + + ) diff --git a/frontend/src/pages/admin/AddBotToChatPage/AddBotToChatPage.tsx b/frontend/src/pages/admin/AddBotToChatPage/AddBotToChatPage.tsx index 2123149c..0693de83 100644 --- a/frontend/src/pages/admin/AddBotToChatPage/AddBotToChatPage.tsx +++ b/frontend/src/pages/admin/AddBotToChatPage/AddBotToChatPage.tsx @@ -18,7 +18,7 @@ import { AdminChat, useChatActions } from '@store' import { findNewChat } from './helpers' -const webApp = window.Telegram.WebApp +const webApp = window.Telegram?.WebApp export const AddBotToChatPage = () => { const { appNavigate } = useAppNavigation() @@ -39,7 +39,7 @@ export const AddBotToChatPage = () => { }, [appNavigate]) const addGatewayBot = useCallback(() => { - webApp.openTelegramLink( + webApp?.openTelegramLink( `${config.botLink}?startgroup=&admin=restrict_members+invite_users` ) setIsCheckingNewChat(true) diff --git a/frontend/src/pages/admin/BotAddedSuccessPage/BotAddedSuccessPage.tsx b/frontend/src/pages/admin/BotAddedSuccessPage/BotAddedSuccessPage.tsx index 06793001..0ed7860b 100644 --- a/frontend/src/pages/admin/BotAddedSuccessPage/BotAddedSuccessPage.tsx +++ b/frontend/src/pages/admin/BotAddedSuccessPage/BotAddedSuccessPage.tsx @@ -15,7 +15,7 @@ import { useParams } from 'react-router-dom' import { useChatActions, useApp, useAppActions } from '@store' -const webApp = window.Telegram.WebApp +const webApp = window.Telegram?.WebApp export const BotAddedSuccessPage = () => { const { chatSlug } = useParams<{ chatSlug: string }>() @@ -30,7 +30,7 @@ export const BotAddedSuccessPage = () => { if (!chatSlug) return try { await fetchChatAction(chatSlug) - webApp.HapticFeedback.notificationOccurred('success') + webApp?.HapticFeedback.notificationOccurred('success') } catch (error) { console.error(error) adminChatNotFound() diff --git a/frontend/src/pages/admin/ChatPage/components/ChatConditions/ChatConditions.tsx b/frontend/src/pages/admin/ChatPage/components/ChatConditions/ChatConditions.tsx index b85c0d15..65ab0deb 100644 --- a/frontend/src/pages/admin/ChatPage/components/ChatConditions/ChatConditions.tsx +++ b/frontend/src/pages/admin/ChatPage/components/ChatConditions/ChatConditions.tsx @@ -36,7 +36,7 @@ import { DraggableCondition } from '../DraggableCondition' import { DroppableGroup } from '../DroppableGroup' import styles from './ChatConditions.module.scss' -const webApp = window.Telegram.WebApp +const webApp = window.Telegram?.WebApp export const ChatConditions = () => { const { appNavigate } = useAppNavigation() @@ -69,7 +69,7 @@ export const ChatConditions = () => { order: order, chatSlug: chat?.slug || '', }) - webApp.HapticFeedback.impactOccurred('soft') + webApp?.HapticFeedback.impactOccurred('soft') } catch (error) { console.error(error) showToast({ @@ -137,7 +137,7 @@ export const ChatConditions = () => { const handleDragStart = (event: DragStartEvent) => { if (!canDrag) return setActiveId(event.active.id as string) - webApp.HapticFeedback.impactOccurred('light') + webApp?.HapticFeedback.impactOccurred('light') } const handleDragEnd = (event: DragEndEvent) => { diff --git a/frontend/src/pages/admin/ChatPage/components/ChatHeader/ChatHeader.tsx b/frontend/src/pages/admin/ChatPage/components/ChatHeader/ChatHeader.tsx index a000ab32..dc04c6e1 100644 --- a/frontend/src/pages/admin/ChatPage/components/ChatHeader/ChatHeader.tsx +++ b/frontend/src/pages/admin/ChatPage/components/ChatHeader/ChatHeader.tsx @@ -23,9 +23,9 @@ export const ChatHeader = () => { const [description, setDescription] = useState(chat?.description ?? '') const isMobile = - webApp.platform === 'ios' || - webApp.platform === 'android' || - webApp.platform === 'android_x' + webApp?.platform === 'ios' || + webApp?.platform === 'android' || + webApp?.platform === 'android_x' const handleChangeDescription = (value: string) => { setDescription(value) @@ -40,12 +40,12 @@ export const ChatHeader = () => { const handleShareLink = () => { if (!chat?.slug) return - webApp.HapticFeedback.impactOccurred('soft') + webApp?.HapticFeedback.impactOccurred('soft') const url = `${config.miniAppLink}?startapp=ch_${chat?.slug}` if (isMobile) { - webApp.openTelegramLink( + webApp?.openTelegramLink( `https://t.me/share/url?url=${encodeURI(url)}&text=${chat.title}` ) } else { @@ -56,7 +56,7 @@ export const ChatHeader = () => { const handleCopyLink = () => { if (!chat?.slug) return const url = `${config.miniAppLink}?startapp=ch_${chat?.slug}` - webApp.HapticFeedback.impactOccurred('soft') + webApp?.HapticFeedback.impactOccurred('soft') copy(url, 'Link copied!') } diff --git a/frontend/src/pages/admin/GrantPermissionsPage/GrantPermissionsPage.tsx b/frontend/src/pages/admin/GrantPermissionsPage/GrantPermissionsPage.tsx index deb54476..6088817c 100644 --- a/frontend/src/pages/admin/GrantPermissionsPage/GrantPermissionsPage.tsx +++ b/frontend/src/pages/admin/GrantPermissionsPage/GrantPermissionsPage.tsx @@ -16,7 +16,7 @@ import { useParams } from 'react-router-dom' import config from '@config' import { useApp, useChatActions, useAppActions } from '@store' -const webApp = window.Telegram.WebApp +const webApp = window.Telegram?.WebApp export const GrantPermissionsPage = () => { const { chatSlug } = useParams<{ chatSlug: string }>() @@ -56,7 +56,7 @@ export const GrantPermissionsPage = () => { } const grantPermissions = () => { - webApp.openTelegramLink( + webApp?.openTelegramLink( `${config.botLink}?startgroup=&admin=restrict_members+invite_users` ) diff --git a/frontend/src/pages/admin/MainPage/MainPage.module.scss b/frontend/src/pages/admin/MainPage/MainPage.module.scss index 7232b674..a386e82a 100644 --- a/frontend/src/pages/admin/MainPage/MainPage.module.scss +++ b/frontend/src/pages/admin/MainPage/MainPage.module.scss @@ -1,6 +1,4 @@ .container { display: flex; flex-direction: column; - height: 100%; - min-height: 0; } diff --git a/frontend/src/pages/admin/MainPage/MainPage.tsx b/frontend/src/pages/admin/MainPage/MainPage.tsx index 85060141..76bf9300 100644 --- a/frontend/src/pages/admin/MainPage/MainPage.tsx +++ b/frontend/src/pages/admin/MainPage/MainPage.tsx @@ -1,12 +1,10 @@ import { - Block, BlockNew, PageLayoutNew, TelegramBackButton, TelegramMainButton, } from '@components' import { Text } from '@components' -import { goTo } from '@utils' import { useCallback } from 'react' import { useNavigate } from 'react-router-dom' @@ -21,18 +19,10 @@ export const MainPage = () => { [] ) - const navigateToToolsPage = () => { - goTo('https://tools.tg') - } - - const handleToProjectPage = () => { - goTo('https://github.com/OpenBuilders/access-tool') - } - return ( ) diff --git a/frontend/src/pages/admin/MainPage/components/ChatsBlock/ChatsBlock.module.scss b/frontend/src/pages/admin/MainPage/components/ChatsBlock/ChatsBlock.module.scss index 5eaae2e4..c8ee645b 100644 --- a/frontend/src/pages/admin/MainPage/components/ChatsBlock/ChatsBlock.module.scss +++ b/frontend/src/pages/admin/MainPage/components/ChatsBlock/ChatsBlock.module.scss @@ -1,35 +1,25 @@ .chatsBlock { display: flex; flex-direction: column; - flex: 1; - min-height: 0; - overflow: hidden; -} - -.contentWrapper { - position: relative; - overflow: hidden; - width: 100%; - min-height: 0; - display: flex; - flex-direction: column; } .contentSlider { display: flex; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); - min-height: 0; } .contentSlide { width: 50%; padding: 0 16px; - flex-shrink: 0; - overflow-y: auto; - min-height: 0; - display: flex; - flex-direction: column; - flex: 1; + opacity: 1; + visibility: visible; + height: 100%; +} + +.contentSlideHide { + opacity: 0; + visibility: hidden; + height: 0; } .orderByContainer { diff --git a/frontend/src/pages/admin/MainPage/components/ChatsBlock/ChatsBlock.tsx b/frontend/src/pages/admin/MainPage/components/ChatsBlock/ChatsBlock.tsx index 2e1ed7d3..9435a36b 100644 --- a/frontend/src/pages/admin/MainPage/components/ChatsBlock/ChatsBlock.tsx +++ b/frontend/src/pages/admin/MainPage/components/ChatsBlock/ChatsBlock.tsx @@ -2,7 +2,7 @@ import { BlockNew, Dropdown, Icon, TabsContainer } from '@components' import { ChatsActiveTab, ChatsPopularOrderBy } from '@types' import { hapticFeedback } from '@utils' import cn from 'classnames' -import { useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useAdminChatsQuery, useChatsPopularQuery } from '@store-new' @@ -24,6 +24,27 @@ export const ChatsBlock = () => { const isLoading = chatsPopularIsLoading || adminChatsIsLoading + useEffect(() => { + const addedElement = document.getElementById(`added-container`) + const exploreElement = document.getElementById(`explore-container`) + + if (!addedElement || !exploreElement) return + + if (activeTab === 'added') { + addedElement.style.height = '100%' + setTimeout(() => { + exploreElement.style.height = '0px' + }, 300) + } + + if (activeTab === 'explore') { + exploreElement.style.height = '100%' + setTimeout(() => { + addedElement.style.height = '0px' + }, 300) + } + }, [activeTab]) + const handleChangeActiveTab = (value: ChatsActiveTab) => { if (isLoading) return hapticFeedback('soft') @@ -90,20 +111,18 @@ export const ChatsBlock = () => { {isLoading ? ( ) : ( -
-
- {contentSlides.map((slide, index) => ( -
- {slide} -
- ))} -
+
+ {contentSlides.map((slide, index) => ( +
+ {slide} +
+ ))}
)} diff --git a/frontend/src/pages/admin/MainPage/components/ChatsBlock/Skeleton.tsx b/frontend/src/pages/admin/MainPage/components/ChatsBlock/Skeleton.tsx index 221289ff..887d1baa 100644 --- a/frontend/src/pages/admin/MainPage/components/ChatsBlock/Skeleton.tsx +++ b/frontend/src/pages/admin/MainPage/components/ChatsBlock/Skeleton.tsx @@ -4,15 +4,15 @@ export const Skeleton = () => { return (
{ + const navigateToToolsPage = () => { + goTo('https://tools.tg') + } + + const handleToProjectPage = () => { + goTo('https://github.com/OpenBuilders/access-tool') + } + return ( + + + This tool is{' '} + + open source + + , created by independent +
+ developers, as part of + + {' '} + Telegram Tools + +
+
+ ) +} diff --git a/frontend/src/pages/admin/MainPage/components/Footer/index.ts b/frontend/src/pages/admin/MainPage/components/Footer/index.ts new file mode 100644 index 00000000..bd2c119a --- /dev/null +++ b/frontend/src/pages/admin/MainPage/components/Footer/index.ts @@ -0,0 +1 @@ +export * from './Footer' diff --git a/frontend/src/pages/admin/MainPage/components/PopularChatsList/PopularChatsList.module.scss b/frontend/src/pages/admin/MainPage/components/PopularChatsList/PopularChatsList.module.scss new file mode 100644 index 00000000..066ed09a --- /dev/null +++ b/frontend/src/pages/admin/MainPage/components/PopularChatsList/PopularChatsList.module.scss @@ -0,0 +1,7 @@ +.divider { + margin-top: 2px; + height: 2px; + width: 2px; + border-radius: var(--border-radius-full); + background-color: var(--color-foreground-secondary); +} diff --git a/frontend/src/pages/admin/MainPage/components/PopularChatsList/PopularChatsList.tsx b/frontend/src/pages/admin/MainPage/components/PopularChatsList/PopularChatsList.tsx index dd5ff913..5b899aac 100644 --- a/frontend/src/pages/admin/MainPage/components/PopularChatsList/PopularChatsList.tsx +++ b/frontend/src/pages/admin/MainPage/components/PopularChatsList/PopularChatsList.tsx @@ -1,40 +1,56 @@ -import { Group, GroupItem, Image, Text } from '@components' +import { BlockNew, Group, GroupItem, Image, Text } from '@components' import { ChatPopular } from '@types' -import { createMembersCount, hapticFeedback } from '@utils' +import { hapticFeedback, pluralize } from '@utils' import { useNavigate } from 'react-router-dom' +import { Footer } from '../Footer' +import styles from './PopularChatsList.module.scss' + interface PopularChatsListProps { chats: ChatPopular[] } export const PopularChatsList = ({ chats }: PopularChatsListProps) => { const navigate = useNavigate() + return ( - - {chats.map((chat) => ( - - {createMembersCount(chat.membersCount)} - - } - chevron - before={ - - } - onClick={() => { - hapticFeedback('soft') - navigate(`/client/${chat.slug}`) - }} - /> - ))} - + + + {chats.map((chat) => ( + + + ${Math.floor(chat.tcv).toLocaleString()} + +
+ + {pluralize( + ['member', 'members', 'members'], + chat.membersCount + )} + + + } + chevron + before={ + + } + onClick={() => { + hapticFeedback('soft') + navigate(`/client/${chat.slug}`) + }} + /> + ))} + +