[frontend]: add recommended channels list
This commit is contained in:
parent
7eca41f8fb
commit
586f2ce77f
@ -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"
|
||||
|
||||
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 { 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 = () => {
|
||||
<SidebarItem key={route.href} route={route} />
|
||||
))}
|
||||
|
||||
{/* <RecommededChannels /> */}
|
||||
<RecommendedChannels />
|
||||
</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 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<typeof useVerifyAccountMutation>;
|
||||
export type VerifyAccountMutationResult = Apollo.MutationResult<VerifyAccountMutation>;
|
||||
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`
|
||||
query FindChannelByUsername($name: String!) {
|
||||
findChannelByUsername(name: $name) {
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
query FindRecommendedChannels {
|
||||
findRecommendedChannels {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
isVerified
|
||||
stream {
|
||||
isLive
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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]);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client';
|
||||
|
||||
import { SERVER_URL } from './constants/url.constants';
|
||||
|
||||
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