From 586f2ce77fa4cb8f15e35c3daea73caeabbbdc34 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Thu, 7 Aug 2025 05:21:33 +0300 Subject: [PATCH] [frontend]: add recommended channels list --- frontend/package.json | 4 +- .../components/layout/sidebar/ChannelItem.tsx | 78 +++++++++++++++++++ .../layout/sidebar/RecommendedChannels.tsx | 39 ++++++++++ .../src/components/layout/sidebar/UserNav.tsx | 3 +- .../src/components/ui/common/Skeleton.tsx | 15 ++++ .../ui/elements/ChannelVerified.tsx | 34 ++++++++ .../src/components/ui/elements/LiveBadge.tsx | 13 ++++ frontend/src/global.d.ts | 10 --- frontend/src/graphql/generated/output.ts | 50 ++++++++++++ .../channels/FindRecommendedChannels.graphql | 11 +++ frontend/src/hooks/useCurrent.ts | 3 - frontend/src/libs/apollo-client.ts | 1 + frontend/src/libs/i18n/global.d.ts | 10 +++ 13 files changed, 255 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/layout/sidebar/ChannelItem.tsx create mode 100644 frontend/src/components/layout/sidebar/RecommendedChannels.tsx create mode 100644 frontend/src/components/ui/common/Skeleton.tsx create mode 100644 frontend/src/components/ui/elements/ChannelVerified.tsx create mode 100644 frontend/src/components/ui/elements/LiveBadge.tsx delete mode 100644 frontend/src/global.d.ts create mode 100644 frontend/src/graphql/queries/channels/FindRecommendedChannels.graphql create mode 100644 frontend/src/libs/i18n/global.d.ts diff --git a/frontend/package.json b/frontend/package.json index daf0fbf..28cb974 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "yarn run codegen && next dev --turbopack", - "build": "yarn run codegen && next build", + "dev": "next dev --turbopack", + "build": "next build", "start": "next start", "lint": "next lint", "codegen": "graphql-codegen --config graphql.config.ts --watch" diff --git a/frontend/src/components/layout/sidebar/ChannelItem.tsx b/frontend/src/components/layout/sidebar/ChannelItem.tsx new file mode 100644 index 0000000..f19412c --- /dev/null +++ b/frontend/src/components/layout/sidebar/ChannelItem.tsx @@ -0,0 +1,78 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import { Button } from '@/components/ui/common/Button'; +import { Skeleton } from '@/components/ui/common/Skeleton'; +import { ChannelAvatar } from '@/components/ui/elements/ChannelAvatar'; +import { ChannelVerified } from '@/components/ui/elements/ChannelVerified'; +import { Hint } from '@/components/ui/elements/Hint'; +import { LiveBadge } from '@/components/ui/elements/LiveBadge'; +import { useSidebar } from '@/hooks/useSidebar'; +import { cn } from '@/utils/tw-merge'; + +import type { FindRecommendedChannelsQuery } from '@/graphql/generated/output'; + +type ChannelItemProps = { + channel: FindRecommendedChannelsQuery['findRecommendedChannels'][0]; +}; + +export const ChannelItem = ({ channel }: ChannelItemProps) => { + const pathname = usePathname(); + const { isCollapsed } = useSidebar(); + + const isActive = pathname === `/${channel.name}`; + + return isCollapsed + ? ( + + + + + + ) + : ( + + ); +}; + +export const ChannelItemSkeleton = () => ; diff --git a/frontend/src/components/layout/sidebar/RecommendedChannels.tsx b/frontend/src/components/layout/sidebar/RecommendedChannels.tsx new file mode 100644 index 0000000..cd4fbf1 --- /dev/null +++ b/frontend/src/components/layout/sidebar/RecommendedChannels.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { useTranslations } from 'next-intl'; + +import { Separator } from '@/components/ui/common/Separator'; +import { useFindRecommendedChannelsQuery } from '@/graphql/generated/output'; +import { useSidebar } from '@/hooks/useSidebar'; + +import { ChannelItem, ChannelItemSkeleton } from './ChannelItem'; + +export const RecommendedChannels = () => { + const t = useTranslations('layout.sidebar.recommended'); + + const { isCollapsed } = useSidebar(); + + const { data, loading: isLoadingRecommended } = useFindRecommendedChannelsQuery(); + const channels = data?.findRecommendedChannels ?? []; + + return ( +
+ + + {!isCollapsed && ( +

+ {t('heading')} +

+ )} + + {isLoadingRecommended + ? Array.from({ length: 7 }).map((_, index) => ( + // eslint-disable-next-line react/no-array-index-key + + )) + : channels.map((channel) => ( + + ))} +
+ ); +}; diff --git a/frontend/src/components/layout/sidebar/UserNav.tsx b/frontend/src/components/layout/sidebar/UserNav.tsx index 3e10e7d..130e6a2 100644 --- a/frontend/src/components/layout/sidebar/UserNav.tsx +++ b/frontend/src/components/layout/sidebar/UserNav.tsx @@ -1,6 +1,7 @@ import { Folder, Home, Radio } from 'lucide-react'; import { useTranslations } from 'next-intl'; +import { RecommendedChannels } from './RecommendedChannels'; import { SidebarItem } from './SidebarItem'; import type { Route } from './route.interface'; @@ -32,7 +33,7 @@ export const UserNav = () => { ))} - {/* */} + ); }; diff --git a/frontend/src/components/ui/common/Skeleton.tsx b/frontend/src/components/ui/common/Skeleton.tsx new file mode 100644 index 0000000..56cd665 --- /dev/null +++ b/frontend/src/components/ui/common/Skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from '@/utils/tw-merge'; + +import type { HTMLAttributes } from 'react'; + +const Skeleton = ({ className, ...props }: HTMLAttributes) => ( +
+); + +export { Skeleton }; diff --git a/frontend/src/components/ui/elements/ChannelVerified.tsx b/frontend/src/components/ui/elements/ChannelVerified.tsx new file mode 100644 index 0000000..87516e5 --- /dev/null +++ b/frontend/src/components/ui/elements/ChannelVerified.tsx @@ -0,0 +1,34 @@ +import { type VariantProps, cva } from 'class-variance-authority'; +import { Check } from 'lucide-react'; + +import { cn } from '@/utils/tw-merge'; + +const channelVerifiedSizes = cva('', { + variants: { + size: { + sm: 'size-3', + default: 'size-4', + }, + }, + defaultVariants: { + size: 'default', + }, +}); + +type ChannelVerifiedProps = VariantProps & {}; + +export const ChannelVerified = ({ size }: ChannelVerifiedProps) => ( + + + +); diff --git a/frontend/src/components/ui/elements/LiveBadge.tsx b/frontend/src/components/ui/elements/LiveBadge.tsx new file mode 100644 index 0000000..36b5c09 --- /dev/null +++ b/frontend/src/components/ui/elements/LiveBadge.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { useTranslations } from 'next-intl'; + +export const LiveBadge = () => { + const t = useTranslations('components.liveBadge'); + + return ( +
+ {t('text')} +
+ ); +}; diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts deleted file mode 100644 index b50b401..0000000 --- a/frontend/src/global.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type messages from '../public/languages/ru.json'; - -const locales = ['ru', 'en'] as const; - -declare module 'next-intl' { - type AppConfig = { - Locale: (typeof locales)[number]; - Messages: typeof messages; - }; -} diff --git a/frontend/src/graphql/generated/output.ts b/frontend/src/graphql/generated/output.ts index fa40bae..62d428d 100644 --- a/frontend/src/graphql/generated/output.ts +++ b/frontend/src/graphql/generated/output.ts @@ -636,6 +636,11 @@ export type VerifyAccountMutationVariables = Exact<{ export type VerifyAccountMutation = { __typename?: 'Mutation', verifyAccount: { __typename?: 'AuthModel', message?: string | null, user?: { __typename?: 'UserModel', isEmailVerified: boolean } | null } }; +export type FindRecommendedChannelsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type FindRecommendedChannelsQuery = { __typename?: 'Query', findRecommendedChannels: Array<{ __typename?: 'UserModel', id: string, name: string, avatar?: string | null, isVerified: boolean, stream: { __typename?: 'StreamModel', isLive: boolean } }> }; + export type FindChannelByUsernameQueryVariables = Exact<{ name: Scalars['String']['input']; }>; @@ -890,6 +895,51 @@ export function useVerifyAccountMutation(baseOptions?: Apollo.MutationHookOption export type VerifyAccountMutationHookResult = ReturnType; export type VerifyAccountMutationResult = Apollo.MutationResult; export type VerifyAccountMutationOptions = Apollo.BaseMutationOptions; +export const FindRecommendedChannelsDocument = gql` + query FindRecommendedChannels { + findRecommendedChannels { + id + name + avatar + isVerified + stream { + isLive + } + } +} + `; + +/** + * __useFindRecommendedChannelsQuery__ + * + * To run a query within a React component, call `useFindRecommendedChannelsQuery` and pass it any options that fit your needs. + * When your component renders, `useFindRecommendedChannelsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useFindRecommendedChannelsQuery({ + * variables: { + * }, + * }); + */ +export function useFindRecommendedChannelsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(FindRecommendedChannelsDocument, options); + } +export function useFindRecommendedChannelsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(FindRecommendedChannelsDocument, options); + } +export function useFindRecommendedChannelsSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(FindRecommendedChannelsDocument, options); + } +export type FindRecommendedChannelsQueryHookResult = ReturnType; +export type FindRecommendedChannelsLazyQueryHookResult = ReturnType; +export type FindRecommendedChannelsSuspenseQueryHookResult = ReturnType; +export type FindRecommendedChannelsQueryResult = Apollo.QueryResult; export const FindChannelByUsernameDocument = gql` query FindChannelByUsername($name: String!) { findChannelByUsername(name: $name) { diff --git a/frontend/src/graphql/queries/channels/FindRecommendedChannels.graphql b/frontend/src/graphql/queries/channels/FindRecommendedChannels.graphql new file mode 100644 index 0000000..0a607a5 --- /dev/null +++ b/frontend/src/graphql/queries/channels/FindRecommendedChannels.graphql @@ -0,0 +1,11 @@ +query FindRecommendedChannels { + findRecommendedChannels { + id + name + avatar + isVerified + stream { + isLive + } + } +} diff --git a/frontend/src/hooks/useCurrent.ts b/frontend/src/hooks/useCurrent.ts index becf550..24d7868 100644 --- a/frontend/src/hooks/useCurrent.ts +++ b/frontend/src/hooks/useCurrent.ts @@ -19,12 +19,9 @@ export function useCurrent() { useEffect(() => { if (error) { - console.log('error', error); if (isAuthenticated) { - console.log('CLEAR'); void clear(); } - console.log('exit'); exit(); } }, [isAuthenticated, exit, clear]); diff --git a/frontend/src/libs/apollo-client.ts b/frontend/src/libs/apollo-client.ts index 7c39c6f..119fd29 100644 --- a/frontend/src/libs/apollo-client.ts +++ b/frontend/src/libs/apollo-client.ts @@ -1,4 +1,5 @@ import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client'; + import { SERVER_URL } from './constants/url.constants'; const httpLink = createHttpLink({ diff --git a/frontend/src/libs/i18n/global.d.ts b/frontend/src/libs/i18n/global.d.ts new file mode 100644 index 0000000..2351127 --- /dev/null +++ b/frontend/src/libs/i18n/global.d.ts @@ -0,0 +1,10 @@ +import type { Language } from './config'; +import type messages from '../../../public/languages/ru.json'; + +export declare module 'next-intl' { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface AppConfig { + Locale: Language; + Messages: typeof messages; + } +}