[frontend]: add recommended channels list
This commit is contained in:
parent
7eca41f8fb
commit
586f2ce77f
@ -3,8 +3,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "yarn run codegen && next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "yarn run codegen && next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"codegen": "graphql-codegen --config graphql.config.ts --watch"
|
"codegen": "graphql-codegen --config graphql.config.ts --watch"
|
||||||
|
|||||||
78
frontend/src/components/layout/sidebar/ChannelItem.tsx
Normal file
78
frontend/src/components/layout/sidebar/ChannelItem.tsx
Normal file
@ -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
|
||||||
|
? (
|
||||||
|
<Hint asChild label={channel.name} side="right">
|
||||||
|
<Link
|
||||||
|
className="mt-3 flex w-full items-center justify-center"
|
||||||
|
href={`/${channel.name}`}
|
||||||
|
>
|
||||||
|
<ChannelAvatar
|
||||||
|
channel={channel}
|
||||||
|
isLive={channel.stream.isLive}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</Hint>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
className={cn(
|
||||||
|
'mt-2 h-11 w-full justify-start',
|
||||||
|
isActive && 'bg-accent',
|
||||||
|
)}
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className="flex w-full items-center"
|
||||||
|
href={`/${channel.name}`}
|
||||||
|
>
|
||||||
|
<ChannelAvatar
|
||||||
|
channel={channel}
|
||||||
|
isLive={channel.stream.isLive}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 className="truncate pl-3">
|
||||||
|
{channel.name}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{channel.isVerified ? <ChannelVerified size="sm" /> : null}
|
||||||
|
|
||||||
|
{channel.stream.isLive
|
||||||
|
? (
|
||||||
|
<div className="absolute right-5">
|
||||||
|
<LiveBadge />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChannelItemSkeleton = () => <Skeleton className="mt-3 h-11 w-full rounded-full" />;
|
||||||
@ -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 (
|
||||||
|
<div>
|
||||||
|
<Separator className="mb-3" />
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<h2 className="mb-2 px-2 text-lg font-semibold text-foreground">
|
||||||
|
{t('heading')}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoadingRecommended
|
||||||
|
? Array.from({ length: 7 }).map((_, index) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<ChannelItemSkeleton key={index} />
|
||||||
|
))
|
||||||
|
: channels.map((channel) => (
|
||||||
|
<ChannelItem key={channel.id} channel={channel} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { Folder, Home, Radio } from 'lucide-react';
|
import { Folder, Home, Radio } from 'lucide-react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
import { RecommendedChannels } from './RecommendedChannels';
|
||||||
import { SidebarItem } from './SidebarItem';
|
import { SidebarItem } from './SidebarItem';
|
||||||
|
|
||||||
import type { Route } from './route.interface';
|
import type { Route } from './route.interface';
|
||||||
@ -32,7 +33,7 @@ export const UserNav = () => {
|
|||||||
<SidebarItem key={route.href} route={route} />
|
<SidebarItem key={route.href} route={route} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* <RecommededChannels /> */}
|
<RecommendedChannels />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
15
frontend/src/components/ui/common/Skeleton.tsx
Normal file
15
frontend/src/components/ui/common/Skeleton.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { cn } from '@/utils/tw-merge';
|
||||||
|
|
||||||
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
const Skeleton = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'animate-pulse rounded-lg bg-card dark:bg-muted',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { Skeleton };
|
||||||
34
frontend/src/components/ui/elements/ChannelVerified.tsx
Normal file
34
frontend/src/components/ui/elements/ChannelVerified.tsx
Normal file
@ -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<typeof channelVerifiedSizes> & {};
|
||||||
|
|
||||||
|
export const ChannelVerified = ({ size }: ChannelVerifiedProps) => (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-center rounded-full bg-primary',
|
||||||
|
channelVerifiedSizes({ size }),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
'stroke-[4px] p-[3px] text-white',
|
||||||
|
size === 'sm' ? 'size-2' : 'size-4',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
13
frontend/src/components/ui/elements/LiveBadge.tsx
Normal file
13
frontend/src/components/ui/elements/LiveBadge.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
export const LiveBadge = () => {
|
||||||
|
const t = useTranslations('components.liveBadge');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-full bg-rose-500 p-0.5 px-2 text-center text-xs font-semibold uppercase tracking-wide text-white">
|
||||||
|
{t('text')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
10
frontend/src/global.d.ts
vendored
10
frontend/src/global.d.ts
vendored
@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -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 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<{
|
export type FindChannelByUsernameQueryVariables = Exact<{
|
||||||
name: Scalars['String']['input'];
|
name: Scalars['String']['input'];
|
||||||
}>;
|
}>;
|
||||||
@ -890,6 +895,51 @@ export function useVerifyAccountMutation(baseOptions?: Apollo.MutationHookOption
|
|||||||
export type VerifyAccountMutationHookResult = ReturnType<typeof useVerifyAccountMutation>;
|
export type VerifyAccountMutationHookResult = ReturnType<typeof useVerifyAccountMutation>;
|
||||||
export type VerifyAccountMutationResult = Apollo.MutationResult<VerifyAccountMutation>;
|
export type VerifyAccountMutationResult = Apollo.MutationResult<VerifyAccountMutation>;
|
||||||
export type VerifyAccountMutationOptions = Apollo.BaseMutationOptions<VerifyAccountMutation, VerifyAccountMutationVariables>;
|
export type VerifyAccountMutationOptions = Apollo.BaseMutationOptions<VerifyAccountMutation, VerifyAccountMutationVariables>;
|
||||||
|
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<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>(FindRecommendedChannelsDocument, options);
|
||||||
|
}
|
||||||
|
export function useFindRecommendedChannelsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>(FindRecommendedChannelsDocument, options);
|
||||||
|
}
|
||||||
|
export function useFindRecommendedChannelsSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>) {
|
||||||
|
const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useSuspenseQuery<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>(FindRecommendedChannelsDocument, options);
|
||||||
|
}
|
||||||
|
export type FindRecommendedChannelsQueryHookResult = ReturnType<typeof useFindRecommendedChannelsQuery>;
|
||||||
|
export type FindRecommendedChannelsLazyQueryHookResult = ReturnType<typeof useFindRecommendedChannelsLazyQuery>;
|
||||||
|
export type FindRecommendedChannelsSuspenseQueryHookResult = ReturnType<typeof useFindRecommendedChannelsSuspenseQuery>;
|
||||||
|
export type FindRecommendedChannelsQueryResult = Apollo.QueryResult<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>;
|
||||||
export const FindChannelByUsernameDocument = gql`
|
export const FindChannelByUsernameDocument = gql`
|
||||||
query FindChannelByUsername($name: String!) {
|
query FindChannelByUsername($name: String!) {
|
||||||
findChannelByUsername(name: $name) {
|
findChannelByUsername(name: $name) {
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
query FindRecommendedChannels {
|
||||||
|
findRecommendedChannels {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
avatar
|
||||||
|
isVerified
|
||||||
|
stream {
|
||||||
|
isLive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,12 +19,9 @@ export function useCurrent() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log('error', error);
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
console.log('CLEAR');
|
|
||||||
void clear();
|
void clear();
|
||||||
}
|
}
|
||||||
console.log('exit');
|
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, exit, clear]);
|
}, [isAuthenticated, exit, clear]);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client';
|
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client';
|
||||||
|
|
||||||
import { SERVER_URL } from './constants/url.constants';
|
import { SERVER_URL } from './constants/url.constants';
|
||||||
|
|
||||||
const httpLink = createHttpLink({
|
const httpLink = createHttpLink({
|
||||||
|
|||||||
10
frontend/src/libs/i18n/global.d.ts
vendored
Normal file
10
frontend/src/libs/i18n/global.d.ts
vendored
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user