diff --git a/frontend/package.json b/frontend/package.json index f9ecfc3..5c5e662 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "@radix-ui/react-switch": "1.2.5", "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "1.2.7", + "@tanstack/react-table": "8.21.3", "apollo-upload-client": "18.0.1", "class-variance-authority": "0.7.1", "clsx": "2.1.1", diff --git a/frontend/src/app/(site)/dashboard/followers/page.tsx b/frontend/src/app/(site)/dashboard/followers/page.tsx new file mode 100644 index 0000000..21dd3e3 --- /dev/null +++ b/frontend/src/app/(site)/dashboard/followers/page.tsx @@ -0,0 +1,20 @@ +import { getTranslations } from 'next-intl/server'; + +import { FollowersTable } from '@/components/features/follow/table/FollowersTable'; +import { NO_INDEX_PAGE } from '@/libs/constants/seo.constants'; + +import type { Metadata } from 'next'; + +export async function generateMetadata(): Promise { + const t = await getTranslations('dashboard.followers.header'); + + return { + title: t('heading'), + description: t('description'), + ...NO_INDEX_PAGE, + }; +} + +const FollowersPage = () => ; + +export default FollowersPage; diff --git a/frontend/src/components/features/follow/table/FollowersTable.tsx b/frontend/src/components/features/follow/table/FollowersTable.tsx new file mode 100644 index 0000000..dc61a0e --- /dev/null +++ b/frontend/src/components/features/follow/table/FollowersTable.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { MoreHorizontal, User } from 'lucide-react'; +import Link from 'next/link'; +import { useTranslations } from 'next-intl'; + +import { Button } from '@/components/ui/common/Button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/common/DropdownMenu'; +import { ChannelAvatar } from '@/components/ui/elements/ChannelAvatar'; +import { ChannelVerified } from '@/components/ui/elements/ChannelVerified'; +import { DataTable, DataTableSkeleton } from '@/components/ui/elements/DataTable'; +import { Heading } from '@/components/ui/elements/Heading'; +import { + type FindMyFollowersQuery, + useFindMyFollowersQuery, +} from '@/graphql/generated/output'; +import { formatDate } from '@/utils/format-date'; + +import type { ColumnDef } from '@tanstack/react-table'; + +const FollowerCell = ({ row }: any) => ( +
+ + +

+ {row.original.follower.name} +

+ + {row.original.follower.isVerified ? : null} +
+); + +const FollowerActionCell = ({ row }: any) => { + const t = useTranslations('dashboard.followers'); + + return ( + + + + + + + + + + + {t('columns.viewChannel')} + + + + + ); +}; + +export const FollowersTable = () => { + const t = useTranslations('dashboard.followers'); + + const { data, loading: isLoadingFollowers } = useFindMyFollowersQuery(); + const followers = data?.findMyFollowers ?? []; + + const followersColumns: ColumnDef[] = [ + { + accessorKey: 'createdAt', + header: t('columns.date'), + cell: ({ row }) => formatDate(row.original.createdAt), + }, + { + accessorKey: 'follower', + header: t('columns.user'), + cell: FollowerCell, + }, + { + accessorKey: 'actions', + header: t('columns.actions'), + cell: FollowerActionCell, + }, + ]; + + return ( +
+ + +
+ { + isLoadingFollowers + ? + : + } +
+
+ ); +}; diff --git a/frontend/src/components/ui/common/Table.tsx b/frontend/src/components/ui/common/Table.tsx new file mode 100644 index 0000000..47d12e6 --- /dev/null +++ b/frontend/src/components/ui/common/Table.tsx @@ -0,0 +1,124 @@ +import { + type HTMLAttributes, + type TdHTMLAttributes, + type ThHTMLAttributes, + forwardRef, +} from 'react'; + +import { cn } from '@/utils/tw-merge'; + +const Table = forwardRef>( + ({ className, ...props }, ref) => ( +
+ + + ), +); +Table.displayName = 'Table'; + +const TableHeader = forwardRef< + HTMLTableSectionElement, + HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = 'TableHeader'; + +const TableBody = forwardRef< + HTMLTableSectionElement, + HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = 'TableBody'; + +const TableFooter = forwardRef< + HTMLTableSectionElement, + HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0', + className, + )} + {...props} + /> +)); +TableFooter.displayName = 'TableFooter'; + +const TableRow = forwardRef< + HTMLTableRowElement, + HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableRow.displayName = 'TableRow'; + +const TableHead = forwardRef< + HTMLTableCellElement, + ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableHead.displayName = 'TableHead'; + +const TableCell = forwardRef< + HTMLTableCellElement, + TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableCell.displayName = 'TableCell'; + +const TableCaption = forwardRef< + HTMLTableCaptionElement, + HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableCaption.displayName = 'TableCaption'; + +export { + Table, + TableBody, + TableCaption, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableRow, +}; diff --git a/frontend/src/components/ui/elements/DataTable.tsx b/frontend/src/components/ui/elements/DataTable.tsx new file mode 100644 index 0000000..de59fac --- /dev/null +++ b/frontend/src/components/ui/elements/DataTable.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { + type ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { Loader } from 'lucide-react'; +import { useTranslations } from 'next-intl'; + +import { Card } from '../common/Card'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '../common/Table'; + +type DataTableProps = { + columns: ColumnDef[]; + data: TData[]; +}; + +export const DataTable = ({ + columns, + data, +}: DataTableProps) => { + const t = useTranslations('components.dataTable'); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext(), + )} + + ))} + + ))} + + + + {table.getRowModel().rows?.length + ? table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + : ( + + + {t('notFound')} + + + )} + +
+
+ ); +}; + +export const DataTableSkeleton = () => ( +
+ + + +
+); diff --git a/frontend/src/graphql/generated/output.ts b/frontend/src/graphql/generated/output.ts index 9a318b2..31eea68 100644 --- a/frontend/src/graphql/generated/output.ts +++ b/frontend/src/graphql/generated/output.ts @@ -764,6 +764,16 @@ 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 FindMyFollowersQueryVariables = Exact<{ [key: string]: never; }>; + + +export type FindMyFollowersQuery = { __typename?: 'Query', findMyFollowers: Array<{ __typename?: 'FollowModel', createdAt: any, follower: { __typename?: 'UserModel', name: string, avatar?: string | null, isVerified: boolean } }> }; + +export type FindMyFollowingsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type FindMyFollowingsQuery = { __typename?: 'Query', findMyFollowings: Array<{ __typename?: 'FollowModel', createdAt: any, followingId: string }> }; + export type FindCurrentSessionQueryVariables = Exact<{ [key: string]: never; }>; @@ -1676,6 +1686,90 @@ export type FindRecommendedChannelsQueryHookResult = ReturnType; export type FindRecommendedChannelsSuspenseQueryHookResult = ReturnType; export type FindRecommendedChannelsQueryResult = Apollo.QueryResult; +export const FindMyFollowersDocument = gql` + query FindMyFollowers { + findMyFollowers { + createdAt + follower { + name + avatar + isVerified + } + } +} + `; + +/** + * __useFindMyFollowersQuery__ + * + * To run a query within a React component, call `useFindMyFollowersQuery` and pass it any options that fit your needs. + * When your component renders, `useFindMyFollowersQuery` 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 } = useFindMyFollowersQuery({ + * variables: { + * }, + * }); + */ +export function useFindMyFollowersQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(FindMyFollowersDocument, options); + } +export function useFindMyFollowersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(FindMyFollowersDocument, options); + } +export function useFindMyFollowersSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(FindMyFollowersDocument, options); + } +export type FindMyFollowersQueryHookResult = ReturnType; +export type FindMyFollowersLazyQueryHookResult = ReturnType; +export type FindMyFollowersSuspenseQueryHookResult = ReturnType; +export type FindMyFollowersQueryResult = Apollo.QueryResult; +export const FindMyFollowingsDocument = gql` + query FindMyFollowings { + findMyFollowings { + createdAt + followingId + } +} + `; + +/** + * __useFindMyFollowingsQuery__ + * + * To run a query within a React component, call `useFindMyFollowingsQuery` and pass it any options that fit your needs. + * When your component renders, `useFindMyFollowingsQuery` 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 } = useFindMyFollowingsQuery({ + * variables: { + * }, + * }); + */ +export function useFindMyFollowingsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(FindMyFollowingsDocument, options); + } +export function useFindMyFollowingsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(FindMyFollowingsDocument, options); + } +export function useFindMyFollowingsSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(FindMyFollowingsDocument, options); + } +export type FindMyFollowingsQueryHookResult = ReturnType; +export type FindMyFollowingsLazyQueryHookResult = ReturnType; +export type FindMyFollowingsSuspenseQueryHookResult = ReturnType; +export type FindMyFollowingsQueryResult = Apollo.QueryResult; export const FindCurrentSessionDocument = gql` query FindCurrentSession { findCurrentSession { diff --git a/frontend/src/graphql/queries/follow/FindMyFollowers.graphql b/frontend/src/graphql/queries/follow/FindMyFollowers.graphql new file mode 100644 index 0000000..909d54c --- /dev/null +++ b/frontend/src/graphql/queries/follow/FindMyFollowers.graphql @@ -0,0 +1,10 @@ +query FindMyFollowers { + findMyFollowers { + createdAt + follower { + name + avatar + isVerified + } + } +} diff --git a/frontend/src/graphql/queries/follow/FindMyFollowings.graphql b/frontend/src/graphql/queries/follow/FindMyFollowings.graphql new file mode 100644 index 0000000..7504dde --- /dev/null +++ b/frontend/src/graphql/queries/follow/FindMyFollowings.graphql @@ -0,0 +1,6 @@ +query FindMyFollowings { + findMyFollowings { + createdAt + followingId + } +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index aff73c8..bd66aeb 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2169,6 +2169,18 @@ postcss "^8.4.41" tailwindcss "4.1.11" +"@tanstack/react-table@8.21.3": + version "8.21.3" + resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.3.tgz#2c38c747a5731c1a07174fda764b9c2b1fb5e91b" + integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww== + dependencies: + "@tanstack/table-core" "8.21.3" + +"@tanstack/table-core@8.21.3": + version "8.21.3" + resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c" + integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg== + "@theguild/federation-composition@^0.19.0": version "0.19.1" resolved "https://registry.yarnpkg.com/@theguild/federation-composition/-/federation-composition-0.19.1.tgz#b3907bfcbdbd69d4628a3b1270936615f56b904f"