From b45a062326b49bb836efff0c6e3c95fdfa9f15e0 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Thu, 31 Jul 2025 00:00:59 -0500 Subject: [PATCH] add new root maintainers page with fun filtering and sorting --- src/components/MaintainerCard.tsx | 190 ++++- src/libraries/maintainers.ts | 75 ++ src/routeTree.gen.ts | 45 + .../$libraryId/$version.docs.contributors.tsx | 140 ++- src/routes/_libraries/maintainers.tsx | 796 ++++++++++++++++++ src/routes/_libraries/paid-support.tsx | 198 +++-- src/routes/_libraries/route.tsx | 6 + src/server/github.ts | 340 +++++++- src/utils/docs.ts | 89 ++ 9 files changed, 1792 insertions(+), 87 deletions(-) create mode 100644 src/routes/_libraries/maintainers.tsx diff --git a/src/components/MaintainerCard.tsx b/src/components/MaintainerCard.tsx index af2c57d55..db93925e7 100644 --- a/src/components/MaintainerCard.tsx +++ b/src/components/MaintainerCard.tsx @@ -5,6 +5,7 @@ import { getPersonsMaintainerOf, } from '~/libraries/maintainers' import { useState } from 'react' +// import { FaCode, FaGitAlt, FaComment, FaEye } from 'react-icons/fa' function RoleBadge({ maintainer, @@ -187,6 +188,69 @@ function MaintainerSocialLinks({ maintainer }: { maintainer: Maintainer }) { ) } +// GitHub stats component - commented out due to performance/accuracy concerns +/* +function GitHubStats({ + username, + stats, +}: { + username: string + stats?: { + totalCommits: number + totalPullRequests: number + totalIssues: number + totalReviews: number + } +}) { + if (!stats) return null + + const totalContributions = + stats.totalCommits + + stats.totalPullRequests + + stats.totalIssues + + stats.totalReviews + + return ( +
+
+ + {stats.totalCommits.toLocaleString()} +
+
+ + {stats.totalPullRequests.toLocaleString()} +
+
+ + {stats.totalIssues.toLocaleString()} +
+
+ + {stats.totalReviews.toLocaleString()} +
+
+ {totalContributions.toLocaleString()} total +
+
+ ) +} +*/ + interface MaintainerCardProps { maintainer: Maintainer libraryId?: Library['id'] @@ -196,6 +260,17 @@ interface CompactMaintainerCardProps { maintainer: Maintainer } +interface MaintainerRowCardProps { + maintainer: Maintainer + libraryId?: Library['id'] + stats?: { + totalCommits: number + totalPullRequests: number + totalIssues: number + totalReviews: number + } +} + export function CompactMaintainerCard({ maintainer, }: CompactMaintainerCardProps) { @@ -226,6 +301,119 @@ export function CompactMaintainerCard({ ) } +export function MaintainerRowCard({ + maintainer, + libraryId, + stats, +}: MaintainerRowCardProps) { + const libraries = getPersonsMaintainerOf(maintainer) + const [showAllLibraries, setShowAllLibraries] = useState(false) + + return ( +
+
+ {/* Avatar Section */} + +
+ {`Avatar +
+
+
+ + {/* Main Content */} +
+
+
+ + {maintainer.name} + + {libraryId && ( + + )} +
+ +
+ +
+
+ + {/* All Pills Inline */} + {((maintainer.frameworkExpertise && + maintainer.frameworkExpertise.length > 0) || + (maintainer.specialties && maintainer.specialties.length > 0) || + (!libraryId && libraries.length > 0)) && ( +
+ {/* Framework chips */} + {maintainer.frameworkExpertise && + maintainer.frameworkExpertise.length > 0 && + maintainer.frameworkExpertise.map((framework) => ( + + ))} + + {/* Specialty chips */} + {maintainer.specialties && + maintainer.specialties.length > 0 && + maintainer.specialties.map((specialty) => ( + + ))} + + {/* Library badges */} + {!libraryId && + libraries.length > 0 && + libraries + .slice(0, showAllLibraries ? undefined : 2) + .map((library) => ( + + ))} + + {/* Show more button */} + {!libraryId && !showAllLibraries && libraries.length > 2 && ( + + )} +
+ )} + + {/* GitHub Stats - commented out due to performance/accuracy concerns */} + {/*
+ +
*/} +
+
+
+ ) +} + export function MaintainerCard({ maintainer, libraryId }: MaintainerCardProps) { const libraries = getPersonsMaintainerOf(maintainer) const [showAllLibraries, setShowAllLibraries] = useState(false) @@ -240,7 +428,7 @@ export function MaintainerCard({ maintainer, libraryId }: MaintainerCardProps) { target="_blank" rel="noopener noreferrer" aria-label={`View ${maintainer.name}'s GitHub profile`} - className="relative h-64 overflow block" + className="relative h-64 overflow-hidden block" tabIndex={0} > 0) return 'creator' + if (person.maintainerOf && person.maintainerOf.length > 0) + return 'maintainer' + if (person.contributorOf && person.contributorOf.length > 0) + return 'contributor' + return 'other' + } + + // Check roles only for the filtered libraries + const isCreatorOfFiltered = libraryIds.some((lib) => + person.creatorOf?.includes(lib) + ) + const isMaintainerOfFiltered = libraryIds.some((lib) => + person.maintainerOf?.includes(lib) + ) + const isContributorOfFiltered = libraryIds.some((lib) => + person.contributorOf?.includes(lib) + ) + + if (isCreatorOfFiltered) return 'creator' + if (isMaintainerOfFiltered) return 'maintainer' + if (isContributorOfFiltered) return 'contributor' + return 'other' +} + +export function getRolePriorityForFilteredLibraries( + person: Maintainer, + libraryIds: Library['id'][] | undefined +): number { + const role = getRoleForFilteredLibraries(person, libraryIds) + + // Higher numbers = higher priority (sorted first) + switch (role) { + case 'creator': + return 4 + case 'maintainer': + return 3 + case 'contributor': + return 2 + case 'other': + return 1 + default: + return 0 + } +} + +export function getIsCoreMaintainerForFilteredLibraries( + person: Maintainer, + libraryIds: Library['id'][] | undefined +): boolean { + // If no libraries are filtered, use global core maintainer status + if (!libraryIds || libraryIds.length === 0) { + return person.isCoreMaintainer || false + } + + // When filtering, core maintainer status is only relevant if they have a role in filtered libraries + const role = getRoleForFilteredLibraries(person, libraryIds) + return role !== 'other' && (person.isCoreMaintainer || false) +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 19372334e..330cb7d68 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -30,6 +30,7 @@ import { Route as LibrariesSupportRouteImport } from './routes/_libraries/suppor import { Route as LibrariesPrivacyRouteImport } from './routes/_libraries/privacy' import { Route as LibrariesPartnersRouteImport } from './routes/_libraries/partners' import { Route as LibrariesPaidSupportRouteImport } from './routes/_libraries/paid-support' +import { Route as LibrariesMaintainersRouteImport } from './routes/_libraries/maintainers' import { Route as LibrariesLearnRouteImport } from './routes/_libraries/learn' import { Route as LibrariesEthosRouteImport } from './routes/_libraries/ethos' import { Route as LibrariesBlogRouteImport } from './routes/_libraries/blog' @@ -130,6 +131,11 @@ const LibrariesPaidSupportRoute = LibrariesPaidSupportRouteImport.update({ path: '/paid-support', getParentRoute: () => LibrariesRouteRoute, } as any) +const LibrariesMaintainersRoute = LibrariesMaintainersRouteImport.update({ + id: '/maintainers', + path: '/maintainers', + getParentRoute: () => LibrariesRouteRoute, +} as any) const LibrariesLearnRoute = LibrariesLearnRouteImport.update({ id: '/learn', path: '/learn', @@ -302,6 +308,7 @@ export interface FileRoutesByFullPath { '/blog': typeof LibrariesBlogRouteWithChildren '/ethos': typeof LibrariesEthosRoute '/learn': typeof LibrariesLearnRoute + '/maintainers': typeof LibrariesMaintainersRoute '/paid-support': typeof LibrariesPaidSupportRoute '/partners': typeof LibrariesPartnersRoute '/privacy': typeof LibrariesPrivacyRoute @@ -341,6 +348,7 @@ export interface FileRoutesByTo { '/$libraryId/$version': typeof LibraryIdVersionRouteWithChildren '/ethos': typeof LibrariesEthosRoute '/learn': typeof LibrariesLearnRoute + '/maintainers': typeof LibrariesMaintainersRoute '/paid-support': typeof LibrariesPaidSupportRoute '/partners': typeof LibrariesPartnersRoute '/privacy': typeof LibrariesPrivacyRoute @@ -383,6 +391,7 @@ export interface FileRoutesById { '/_libraries/blog': typeof LibrariesBlogRouteWithChildren '/_libraries/ethos': typeof LibrariesEthosRoute '/_libraries/learn': typeof LibrariesLearnRoute + '/_libraries/maintainers': typeof LibrariesMaintainersRoute '/_libraries/paid-support': typeof LibrariesPaidSupportRoute '/_libraries/partners': typeof LibrariesPartnersRoute '/_libraries/privacy': typeof LibrariesPrivacyRoute @@ -426,6 +435,7 @@ export interface FileRouteTypes { | '/blog' | '/ethos' | '/learn' + | '/maintainers' | '/paid-support' | '/partners' | '/privacy' @@ -465,6 +475,7 @@ export interface FileRouteTypes { | '/$libraryId/$version' | '/ethos' | '/learn' + | '/maintainers' | '/paid-support' | '/partners' | '/privacy' @@ -506,6 +517,7 @@ export interface FileRouteTypes { | '/_libraries/blog' | '/_libraries/ethos' | '/_libraries/learn' + | '/_libraries/maintainers' | '/_libraries/paid-support' | '/_libraries/partners' | '/_libraries/privacy' @@ -653,6 +665,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LibrariesLearnRouteImport parentRoute: typeof LibrariesRouteRoute } + '/_libraries/maintainers': { + id: '/_libraries/maintainers' + path: '/maintainers' + fullPath: '/maintainers' + preLoaderRoute: typeof LibrariesMaintainersRouteImport + parentRoute: typeof LibrariesRouteRoute + } '/_libraries/paid-support': { id: '/_libraries/paid-support' path: '/paid-support' @@ -951,6 +970,13 @@ declare module '@tanstack/react-start/server' { preLoaderRoute: unknown parentRoute: typeof rootServerRouteImport } + '/_libraries/maintainers': { + id: '/_libraries/maintainers' + path: '/maintainers' + fullPath: '/maintainers' + preLoaderRoute: unknown + parentRoute: typeof rootServerRouteImport + } '/_libraries/paid-support': { id: '/_libraries/paid-support' path: '/paid-support' @@ -1348,6 +1374,23 @@ declare module './routes/_libraries/learn' { unknown > } +declare module './routes/_libraries/maintainers' { + const createFileRoute: CreateFileRoute< + '/_libraries/maintainers', + FileRoutesByPath['/_libraries/maintainers']['parentRoute'], + FileRoutesByPath['/_libraries/maintainers']['id'], + FileRoutesByPath['/_libraries/maintainers']['path'], + FileRoutesByPath['/_libraries/maintainers']['fullPath'] + > + + const createServerFileRoute: CreateServerFileRoute< + ServerFileRoutesByPath['/_libraries/maintainers']['parentRoute'], + ServerFileRoutesByPath['/_libraries/maintainers']['id'], + ServerFileRoutesByPath['/_libraries/maintainers']['path'], + ServerFileRoutesByPath['/_libraries/maintainers']['fullPath'], + unknown + > +} declare module './routes/_libraries/paid-support' { const createFileRoute: CreateFileRoute< '/_libraries/paid-support', @@ -1963,6 +2006,7 @@ interface LibrariesRouteRouteChildren { LibrariesBlogRoute: typeof LibrariesBlogRouteWithChildren LibrariesEthosRoute: typeof LibrariesEthosRoute LibrariesLearnRoute: typeof LibrariesLearnRoute + LibrariesMaintainersRoute: typeof LibrariesMaintainersRoute LibrariesPaidSupportRoute: typeof LibrariesPaidSupportRoute LibrariesPartnersRoute: typeof LibrariesPartnersRoute LibrariesPrivacyRoute: typeof LibrariesPrivacyRoute @@ -1986,6 +2030,7 @@ const LibrariesRouteRouteChildren: LibrariesRouteRouteChildren = { LibrariesBlogRoute: LibrariesBlogRouteWithChildren, LibrariesEthosRoute: LibrariesEthosRoute, LibrariesLearnRoute: LibrariesLearnRoute, + LibrariesMaintainersRoute: LibrariesMaintainersRoute, LibrariesPaidSupportRoute: LibrariesPaidSupportRoute, LibrariesPartnersRoute: LibrariesPartnersRoute, LibrariesPrivacyRoute: LibrariesPrivacyRoute, diff --git a/src/routes/$libraryId/$version.docs.contributors.tsx b/src/routes/$libraryId/$version.docs.contributors.tsx index 40c2d1f56..aeccd362e 100644 --- a/src/routes/$libraryId/$version.docs.contributors.tsx +++ b/src/routes/$libraryId/$version.docs.contributors.tsx @@ -5,7 +5,13 @@ import { getLibrary } from '~/libraries' import { ContributorsWall } from '~/components/ContributorsWall' import {} from '@tanstack/react-router' import { getLibraryContributors } from '~/libraries/maintainers' -import { MaintainerCard } from '~/components/MaintainerCard' +import { + MaintainerCard, + CompactMaintainerCard, + MaintainerRowCard, +} from '~/components/MaintainerCard' +import { MdViewList, MdViewModule, MdFormatListBulleted } from 'react-icons/md' +import { useState } from 'react' export const Route = createFileRoute({ component: RouteComponent, @@ -14,42 +20,122 @@ export const Route = createFileRoute({ function RouteComponent() { const { libraryId } = Route.useParams() const library = getLibrary(libraryId) + const [viewMode, setViewMode] = useState<'compact' | 'full' | 'row'>('full') // Get the maintainers for this library const libraryContributors = getLibraryContributors(libraryId) return ( - -
+ <> + +
- {library.name} Maintainers and Contributors -
-
-
- {libraryContributors.map((maintainer) => ( - - ))} +
+ {library.name} Maintainers and Contributors +
+ + {/* View Mode Toggle */} +
+
+ + + +
-
-
-
+
+
+ {libraryContributors.map((maintainer, index) => ( +
+ {viewMode === 'compact' ? ( + + ) : viewMode === 'row' ? ( + + ) : ( + + )} +
+ ))} +
+
+ +
+
-

All-Time Contributors

- -
+

All-Time Contributors

+ +
+
-
- + + ) } diff --git a/src/routes/_libraries/maintainers.tsx b/src/routes/_libraries/maintainers.tsx new file mode 100644 index 000000000..0646699e2 --- /dev/null +++ b/src/routes/_libraries/maintainers.tsx @@ -0,0 +1,796 @@ +import { z } from 'zod' +import { useState } from 'react' +import * as React from 'react' +import { + MdClose, + MdFilterList, + MdViewList, + MdViewModule, + MdFormatListBulleted, +} from 'react-icons/md' +import { Footer } from '~/components/Footer' +import { + MaintainerCard, + CompactMaintainerCard, + MaintainerRowCard, +} from '~/components/MaintainerCard' +import { seo } from '~/utils/seo' +import { + allMaintainers, + Maintainer, + getPersonsMaintainerOf, + getRolePriorityForFilteredLibraries, + getIsCoreMaintainerForFilteredLibraries, + getRoleForFilteredLibraries, +} from '~/libraries/maintainers' +import { Library, libraries } from '~/libraries' +// import { fetchAllMaintainerStats } from '~/utils/docs' + +const librarySchema = z.enum([ + 'start', + 'router', + 'query', + 'table', + 'form', + 'virtual', + 'ranger', + 'store', + 'pacer', + 'db', + 'config', + 'react-charts', + 'create-tsrouter-app', +]) + +const viewModeSchema = z.enum(['compact', 'full', 'row']) +const groupBySchema = z.enum(['none', 'core', 'library', 'role']) +const sortBySchema = z.enum(['none', 'name', 'role', 'contributions']) + +export const Route = createFileRoute({ + component: RouteComponent, + validateSearch: z.object({ + libraries: z.array(librarySchema).optional().catch(undefined), + viewMode: viewModeSchema.optional().default('compact').catch('compact'), + groupBy: groupBySchema.optional().default('none').catch('none'), + sortBy: sortBySchema.optional().default('none').catch('none'), + }), + head: () => ({ + meta: seo({ + title: 'Maintainers | TanStack', + description: + 'Meet the core maintainers and contributors who make TanStack possible', + keywords: 'tanstack,maintainers,contributors,open source,developers', + }), + }), + // loader: async ({ context: { queryClient } }) => { + // try { + // // Fetch GitHub stats for all maintainers + // const stats = await queryClient.ensureQueryData({ + // queryKey: ['maintainerStats'], + // queryFn: () => fetchAllMaintainerStats(), + // staleTime: 1000 * 60 * 30, // 30 minutes + // }) + + // return { + // stats, + // } + // } catch (error) { + // console.error('Error loading maintainer stats:', error) + // // Return empty stats array if there's an error + // return { + // stats: [], + // } + // } + // }, +}) + +interface FilterProps { + selectedLibraries: Library['id'][] | undefined + viewMode: 'compact' | 'full' | 'row' + groupBy: 'none' | 'core' | 'library' | 'role' + sortBy: 'none' | 'name' | 'role' | 'contributions' + onLibrariesChange: (libraries: Library['id'][] | undefined) => void + onViewModeChange: (mode: 'compact' | 'full' | 'row') => void + onGroupByChange: (groupBy: 'none' | 'core' | 'library' | 'role') => void + onSortByChange: (sortBy: 'none' | 'name' | 'role' | 'contributions') => void + onClearAll: () => void +} + +function MaintainersFilter({ + selectedLibraries, + viewMode, + groupBy, + sortBy, + onLibrariesChange, + onViewModeChange, + onGroupByChange, + onSortByChange, + onClearAll, +}: FilterProps) { + const [isOpen, setIsOpen] = useState(false) + + const availableLibraries = libraries.map((lib) => ({ + id: lib.id, + name: lib.name, + bgStyle: lib.bgStyle, + })) + + const toggleLibrary = (libraryId: Library['id']) => { + if (!selectedLibraries) { + onLibrariesChange([libraryId]) + return + } + + if (selectedLibraries.includes(libraryId)) { + const newLibraries = selectedLibraries.filter((id) => id !== libraryId) + onLibrariesChange(newLibraries.length > 0 ? newLibraries : undefined) + } else { + onLibrariesChange([...selectedLibraries, libraryId]) + } + } + + const clearFilters = () => { + onClearAll() + setIsOpen(false) + } + + // Close dropdown when clicking outside + React.useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element + if (!target.closest('[data-filter-dropdown]')) { + setIsOpen(false) + } + } + + if (isOpen) { + document.addEventListener('click', handleClickOutside) + return () => document.removeEventListener('click', handleClickOutside) + } + }, [isOpen]) + + const hasFilters = + (selectedLibraries && selectedLibraries.length > 0) || + groupBy !== 'none' || + sortBy !== 'none' + const filterCount = + (selectedLibraries?.length || 0) + + (groupBy !== 'none' ? 1 : 0) + + (sortBy !== 'none' ? 1 : 0) + + return ( +
+
+ {/* View Mode Toggle */} +
+ + + +
+ + {/* Filter dropdown trigger */} +
+ + + {/* Dropdown menu */} + {isOpen && ( +
+
+
+ + Filter & Sort Options + + {hasFilters && ( + + )} +
+ + {/* Group By */} +
+ + +
+ + {/* Sort By */} +
+ + +
+ + {/* Library filter */} +
+ +
+ {availableLibraries.map((library) => { + const isSelected = + selectedLibraries?.includes(library.id) || false + + return ( + + ) + })} +
+
+
+
+ )} +
+ + {/* Current filter chips */} +
+ {selectedLibraries?.map((libraryId) => { + const library = availableLibraries.find( + (lib) => lib.id === libraryId + ) + return ( + + {library?.name || libraryId} + + + ) + })} +
+
+
+ ) +} + +function MaintainerGrid({ + maintainers, + viewMode, + title, + stats, +}: { + maintainers: Maintainer[] + viewMode: 'compact' | 'full' | 'row' + title?: string + stats?: Array<{ + username: string + totalCommits: number + totalPullRequests: number + totalIssues: number + totalReviews: number + }> +}) { + return ( +
+ {title &&

{title}

} +
+ {maintainers.map((maintainer, index) => ( +
+ {viewMode === 'compact' ? ( + + ) : viewMode === 'row' ? ( + s.username === maintainer.github)} + /> + ) : ( + + )} +
+ ))} +
+
+ ) +} + +function RouteComponent() { + const search = Route.useSearch() + const navigate = Route.useNavigate() + // const loaderData = Route.useLoaderData() + // const stats = loaderData?.stats || [] + const stats: any[] = [] // Empty array since stats are commented out + + const updateFilters = (updates: { + libraries?: Library['id'][] | undefined + viewMode?: 'compact' | 'full' | 'row' + groupBy?: 'none' | 'core' | 'library' | 'role' + sortBy?: 'none' | 'name' | 'role' | 'contributions' + }) => { + navigate({ + search: { + ...search, + ...updates, + }, + replace: true, + }) + } + + // Filter maintainers based on selected criteria + const filteredMaintainers = React.useMemo(() => { + let filtered = [...allMaintainers] + + // Filter by libraries + if (search.libraries && search.libraries.length > 0) { + filtered = filtered.filter((maintainer) => { + const maintainerLibraries = getPersonsMaintainerOf(maintainer) + return ( + maintainerLibraries.some((lib) => + search.libraries!.includes(lib.id as Library['id']) + ) || + maintainer.creatorOf?.some((lib) => + search.libraries!.includes(lib) + ) || + maintainer.maintainerOf?.some((lib) => + search.libraries!.includes(lib) + ) || + maintainer.contributorOf?.some((lib) => + search.libraries!.includes(lib) + ) || + maintainer.consultantOf?.some((lib) => + search.libraries!.includes(lib) + ) + ) + }) + } + + // Sort maintainers + filtered.sort((a, b) => { + // Get original indices to preserve order + const aIndex = allMaintainers.findIndex((m) => m.github === a.github) + const bIndex = allMaintainers.findIndex((m) => m.github === b.github) + + switch (search.sortBy) { + case 'name': + return a.name.localeCompare(b.name) + case 'role': + // Sort by role priority within filtered libraries, then core status, then original order + const aPriority = getRolePriorityForFilteredLibraries( + a, + search.libraries + ) + const bPriority = getRolePriorityForFilteredLibraries( + b, + search.libraries + ) + + if (aPriority !== bPriority) { + return bPriority - aPriority // Higher priority first + } + + // Same role priority - use core maintainer status for filtered libraries + const aIsCore = getIsCoreMaintainerForFilteredLibraries( + a, + search.libraries + ) + const bIsCore = getIsCoreMaintainerForFilteredLibraries( + b, + search.libraries + ) + + if (aIsCore && !bIsCore) return -1 + if (!aIsCore && bIsCore) return 1 + + return aIndex - bIndex + case 'contributions': + // Sort by number of libraries they're involved with + const aLibs = + (a.creatorOf?.length || 0) + + (a.maintainerOf?.length || 0) + + (a.contributorOf?.length || 0) + const bLibs = + (b.creatorOf?.length || 0) + + (b.maintainerOf?.length || 0) + + (b.contributorOf?.length || 0) + return bLibs - aLibs || aIndex - bIndex + case 'none': + // No sorting: Preserve original order from allMaintainers array + return aIndex - bIndex + default: + // Default: Preserve original order from allMaintainers array + return aIndex - bIndex + } + }) + + return filtered + }, [search.libraries, search.sortBy]) + + // Group maintainers based on groupBy setting + const groupedMaintainers = React.useMemo(() => { + switch (search.groupBy) { + case 'core': + const coreMaintainers = filteredMaintainers.filter( + (m) => m.isCoreMaintainer + ) + const regularMaintainers = filteredMaintainers.filter( + (m) => + !m.isCoreMaintainer && + ((m.creatorOf && m.creatorOf.length > 0) || + (m.maintainerOf && m.maintainerOf.length > 0)) + ) + const regularContributors = filteredMaintainers.filter( + (m) => + !m.isCoreMaintainer && + (!m.creatorOf || m.creatorOf.length === 0) && + (!m.maintainerOf || m.maintainerOf.length === 0) + ) + + // Sort by original order within each group + const sortByOriginalOrder = (group: Maintainer[]) => { + return group.sort((a, b) => { + const aIndex = allMaintainers.findIndex( + (m) => m.github === a.github + ) + const bIndex = allMaintainers.findIndex( + (m) => m.github === b.github + ) + return aIndex - bIndex + }) + } + + return [ + { + title: 'Core Maintainers', + maintainers: sortByOriginalOrder(coreMaintainers), + }, + { + title: 'Maintainers', + maintainers: sortByOriginalOrder(regularMaintainers), + }, + { + title: 'Contributors', + maintainers: sortByOriginalOrder(regularContributors), + }, + ].filter((group) => group.maintainers.length > 0) + + case 'library': + const byLibrary = new Map() + filteredMaintainers.forEach((maintainer) => { + // Get all libraries this person is involved with (maintainer, creator, contributor) + const maintainerLibs = getPersonsMaintainerOf(maintainer) + const contributorLibs = + maintainer.contributorOf + ?.map((id) => libraries.find((lib) => lib.id === id)) + .filter(Boolean) || [] + const allLibs = [...maintainerLibs, ...contributorLibs] + + if (allLibs.length === 0) { + // Only put in "Other" if they have no library associations at all + const existing = byLibrary.get('Other') || [] + byLibrary.set('Other', [...existing, maintainer]) + } else { + allLibs.forEach((lib) => { + if (lib) { + const existing = byLibrary.get(lib.name) || [] + byLibrary.set(lib.name, [...existing, maintainer]) + } + }) + } + }) + + // Sort maintainers within each library group: creators first, then maintainers, then contributors + return Array.from(byLibrary.entries()).map(([title, maintainers]) => { + const uniqueMaintainers = [...new Set(maintainers)] + const sortedMaintainers = uniqueMaintainers.sort((a, b) => { + const aIndex = allMaintainers.findIndex( + (m) => m.github === a.github + ) + const bIndex = allMaintainers.findIndex( + (m) => m.github === b.github + ) + + // Get library ID for this group + const lib = libraries.find((l) => l.name === title) + if (!lib) return aIndex - bIndex + + // Sort by role hierarchy within this library + const aIsCreator = a.creatorOf?.includes(lib.id) + const bIsCreator = b.creatorOf?.includes(lib.id) + const aIsMaintainer = a.maintainerOf?.includes(lib.id) + const bIsMaintainer = b.maintainerOf?.includes(lib.id) + + if (aIsCreator && !bIsCreator) return -1 + if (!aIsCreator && bIsCreator) return 1 + if (aIsMaintainer && !bIsMaintainer) return -1 + if (!aIsMaintainer && bIsMaintainer) return 1 + + // Same role level - use original order + return aIndex - bIndex + }) + + return { + title, + maintainers: sortedMaintainers, + } + }) + + case 'role': + const creators = filteredMaintainers.filter( + (m) => getRoleForFilteredLibraries(m, search.libraries) === 'creator' + ) + const libraryMaintainers = filteredMaintainers.filter( + (m) => + getRoleForFilteredLibraries(m, search.libraries) === 'maintainer' + ) + const libraryContributors = filteredMaintainers.filter( + (m) => + getRoleForFilteredLibraries(m, search.libraries) === 'contributor' + ) + + // Sort each role group: core maintainers first, then by original order + const sortRoleGroup = (group: Maintainer[]) => { + return group.sort((a, b) => { + const aIndex = allMaintainers.findIndex( + (m) => m.github === a.github + ) + const bIndex = allMaintainers.findIndex( + (m) => m.github === b.github + ) + + // Core maintainers first within each role + if (a.isCoreMaintainer && !b.isCoreMaintainer) return -1 + if (!a.isCoreMaintainer && b.isCoreMaintainer) return 1 + + // Then by original order + return aIndex - bIndex + }) + } + + return [ + { title: 'Creators', maintainers: sortRoleGroup(creators) }, + { + title: 'Maintainers', + maintainers: sortRoleGroup(libraryMaintainers), + }, + { + title: 'Contributors', + maintainers: sortRoleGroup(libraryContributors), + }, + ].filter((group) => group.maintainers.length > 0) + + default: + return [{ title: '', maintainers: filteredMaintainers }] + } + }, [filteredMaintainers, search.groupBy, search.libraries]) + + const hasResults = filteredMaintainers.length > 0 + const hasFilters = + (search.libraries && search.libraries.length > 0) || + search.groupBy !== 'none' || + search.sortBy !== 'none' + + return ( + <> + + +
+
+
+

+ Maintainers & Contributors +

+

+ Meet the amazing developers who make TanStack possible through + their contributions, maintenance, and dedication to open source +

+
+ + updateFilters({ libraries })} + onViewModeChange={(viewMode) => updateFilters({ viewMode })} + onGroupByChange={(groupBy) => updateFilters({ groupBy })} + onSortByChange={(sortBy) => updateFilters({ sortBy })} + onClearAll={() => navigate({ search: {}, replace: true })} + /> + + {hasResults ? ( +
+ {hasFilters && ( +

+ Showing {filteredMaintainers.length} maintainer + {filteredMaintainers.length === 1 ? '' : 's'} + {search.libraries && search.libraries.length > 0 && ( + + {' '} + for{' '} + + {search.libraries.join(', ')} + + + )} +

+ )} + + {groupedMaintainers.map((group, index) => ( + + ))} +
+ ) : ( +
+
+

+ No maintainers found for the selected filters. +

+ +
+
+ )} + +
+

Want to Contribute?

+

+ TanStack is always welcoming new contributors! Check out our + repositories and join our vibrant community. +

+ +
+ + {/* Disclaimer */} +
+

+ Note: This list showcases + some of our most active maintainers and contributors who have been + with or around TanStack for an extended period and have agreed to + be featured here. However, this represents only a fraction of the + incredible community that makes TanStack possible. Over the years,{' '} + thousands of developers have + contributed code, documentation, bug reports, and ideas that have + shaped these libraries. We're deeply grateful to every single + contributor, whether listed here or not. +

+
+
+
+
+ + ) +} diff --git a/src/routes/_libraries/paid-support.tsx b/src/routes/_libraries/paid-support.tsx index b4965f46c..5e090bfc3 100644 --- a/src/routes/_libraries/paid-support.tsx +++ b/src/routes/_libraries/paid-support.tsx @@ -1,9 +1,14 @@ -import { Link } from '@tanstack/react-router' import { seo } from '~/utils/seo' import { HiOutlineMail } from 'react-icons/hi' +import { MdViewList, MdViewModule, MdFormatListBulleted } from 'react-icons/md' +import { useState } from 'react' import { useScript } from '~/hooks/useScript' import { coreMaintainers } from '~/libraries/maintainers' -import { CompactMaintainerCard } from '~/components/MaintainerCard' +import { + CompactMaintainerCard, + MaintainerCard, + MaintainerRowCard, +} from '~/components/MaintainerCard' export const Route = createFileRoute({ component: PaidSupportComp, @@ -20,6 +25,10 @@ export const Route = createFileRoute({ }) function PaidSupportComp() { + const [viewMode, setViewMode] = useState<'compact' | 'full' | 'row'>( + 'compact' + ) + useScript({ id: 'hs-script-loader', async: true, @@ -28,69 +37,144 @@ function PaidSupportComp() { }) return ( -
-
-
-

- Paid Support -

-

- Private consultation and enterprise paid support for projects of any - size, backed by TanStack's core team -

-
+ <> + -
-
- {coreMaintainers.map((maintainer) => ( - - ))} -
-
+
+
+
+

+ Paid Support +

+

+ Private consultation and enterprise paid support for projects of + any size, backed by TanStack's core team +

+
-
-

- Get Unblocked, Fix Bugs, Accelerate Success -

-

- Our team will help you solve complex problems, debug tricky issues, - and guide your project to success with expert TanStack knowledge. -

- - - Contact Support Team - -
+ {/* View Mode Toggle */} +
+
+ + + +
+
-
-

- For general support questions, free help is available in our{' '} - +

- Discord - {' '} - and{' '} + {coreMaintainers.map((maintainer, index) => ( +
+ {viewMode === 'compact' ? ( + + ) : viewMode === 'row' ? ( + + ) : ( + + )} +
+ ))} +
+
+ +
+

+ Get Unblocked, Fix Bugs, Accelerate Success +

+

+ Our team will help you solve complex problems, debug tricky + issues, and guide your project to success with expert TanStack + knowledge. +

- GitHub Discussions + + Contact Support Team -

+
+ +
+

+ For general support questions, free help is available in our{' '} + + Discord + {' '} + and{' '} + + GitHub Discussions + +

+
-
+ ) } diff --git a/src/routes/_libraries/route.tsx b/src/routes/_libraries/route.tsx index e29b375c3..170eb43e9 100644 --- a/src/routes/_libraries/route.tsx +++ b/src/routes/_libraries/route.tsx @@ -6,6 +6,7 @@ import { twMerge } from 'tailwind-merge' import { sortBy } from '~/utils/utils' import logoColor100w from '~/images/logo-color-100w.png' import { + FaCode, FaDiscord, FaGithub, FaInstagram, @@ -165,6 +166,11 @@ function LibrariesLayout() {
{[ + { + label: 'Maintainers', + icon: , + to: '/maintainers', + }, { label: 'Partners', icon: , diff --git a/src/server/github.ts b/src/server/github.ts index 072c957d7..5b5c05951 100644 --- a/src/server/github.ts +++ b/src/server/github.ts @@ -18,7 +18,15 @@ export const graphqlWithAuth = graphql.defaults({ const githubClientID = 'Iv1.3aa8d13a4a3fde91' const githubClientSecret = 'e2340f390f956b6fbfb9c6f85100d6cfe07f29a8' -export async function exchangeGithubCodeForToken({ code, state, redirectUrl }) { +export async function exchangeGithubCodeForToken({ + code, + state, + redirectUrl, +}: { + code: string + state: string + redirectUrl: string +}) { try { const { data } = await axios.post( 'https://github.com/login/oauth/access_token', @@ -43,7 +51,7 @@ export async function exchangeGithubCodeForToken({ code, state, redirectUrl }) { } } -export async function getTeamBySlug(slug) { +export async function getTeamBySlug(slug: string) { const teams = await octokit.teams.list({ org: GITHUB_ORG, }) @@ -56,3 +64,331 @@ export async function getTeamBySlug(slug) { return sponsorsTeam } + +// GitHub contributor stats - commented out due to performance/accuracy concerns +/* +export interface ContributorStats { + username: string + totalCommits: number + totalPullRequests: number + totalIssues: number + totalReviews: number + firstContribution: string | null + lastContribution: string | null + repositories: Array<{ + name: string + commits: number + pullRequests: number + issues: number + reviews: number + }> +} + +export async function getContributorStats( + username: string +): Promise { + try { + // Use GraphQL for TanStack organization stats + const query = ` + query($username: String!, $org: String!) { + user(login: $username) { + contributionsCollection(organizationID: $org) { + totalCommitContributions + totalPullRequestReviewContributions + totalIssueContributions + totalPullRequestContributions + contributionCalendar { + totalContributions + } + } + } + organization(login: $org) { + id + } + } + ` + + const result = (await graphqlWithAuth(query, { + username, + org: GITHUB_ORG, + })) as any + + const user = result.user + + return { + username, + totalCommits: + user?.contributionsCollection?.totalCommitContributions || 0, + totalPullRequests: + user?.contributionsCollection?.totalPullRequestContributions || 0, + totalIssues: user?.contributionsCollection?.totalIssueContributions || 0, + totalReviews: + user?.contributionsCollection?.totalPullRequestReviewContributions || 0, + firstContribution: null, // GraphQL doesn't provide this easily + lastContribution: null, // GraphQL doesn't provide this easily + repositories: [], // Not tracking per-repo anymore + } + } catch (error) { + console.error(`Error fetching stats for ${username}:`, error) + return { + username, + totalCommits: 0, + totalPullRequests: 0, + totalIssues: 0, + totalReviews: 0, + firstContribution: null, + lastContribution: null, + repositories: [], + } + } +} + +async function getContributorStatsForRepo(username: string, repoName: string) { + const stats = { + commits: 0, + pullRequests: 0, + issues: 0, + reviews: 0, + dates: [] as string[], + } + + try { + // Get commits by the user + const commits = await octokit.repos.listCommits({ + owner: GITHUB_ORG, + repo: repoName, + author: username, + per_page: 100, + }) + + stats.commits = commits.data.length + stats.dates.push( + ...commits.data + .map((commit) => commit.commit.author?.date || '') + .filter(Boolean) + ) + + // Get pull requests by the user + const pullRequests = await octokit.pulls.list({ + owner: GITHUB_ORG, + repo: repoName, + state: 'all', + per_page: 100, + }) + + const userPRs = pullRequests.data.filter( + (pr) => pr.user?.login === username + ) + stats.pullRequests = userPRs.length + stats.dates.push(...userPRs.map((pr) => pr.created_at).filter(Boolean)) + + // Get issues by the user + const issues = await octokit.issues.listForRepo({ + owner: GITHUB_ORG, + repo: repoName, + state: 'all', + per_page: 100, + }) + + const userIssues = issues.data.filter( + (issue) => issue.user?.login === username + ) + stats.issues = userIssues.length + stats.dates.push( + ...userIssues.map((issue) => issue.created_at).filter(Boolean) + ) + + // Get reviews by the user + const reviews = await octokit.pulls.listReviews({ + owner: GITHUB_ORG, + repo: repoName, + pull_number: 1, // We'll need to iterate through all PRs for reviews + }) + + // Note: This is a simplified approach. For accurate review counts, + // we'd need to iterate through all PRs and get reviews for each + const userReviews = reviews.data.filter( + (review) => review.user?.login === username + ) + stats.reviews = userReviews.length + stats.dates.push( + ...userReviews.map((review) => review.submitted_at).filter(Boolean) + ) + } catch (error) { + console.error(`Error fetching stats for ${username} in ${repoName}:`, error) + } + + return stats +} + +export async function getContributorStatsForLibrary( + username: string, + libraryRepo: string +): Promise { + try { + const repoStats = await getContributorStatsForRepo(username, libraryRepo) + + return { + username, + totalCommits: repoStats.commits, + totalPullRequests: repoStats.pullRequests, + totalIssues: repoStats.issues, + totalReviews: repoStats.reviews, + firstContribution: + repoStats.dates.length > 0 ? repoStats.dates.sort()[0] : null, + lastContribution: + repoStats.dates.length > 0 ? repoStats.dates.sort().reverse()[0] : null, + repositories: [ + { + name: libraryRepo, + commits: repoStats.commits, + pullRequests: repoStats.pullRequests, + issues: repoStats.issues, + reviews: repoStats.reviews, + }, + ], + } + } catch (error) { + console.error( + `Error fetching library stats for ${username} in ${libraryRepo}:`, + error + ) + return { + username, + totalCommits: 0, + totalPullRequests: 0, + totalIssues: 0, + totalReviews: 0, + firstContribution: null, + lastContribution: null, + repositories: [], + } + } +} + +// GraphQL approach for more efficient data fetching +export async function getContributorStatsGraphQL( + username: string +): Promise { + try { + const query = ` + query($username: String!, $org: String!) { + user(login: $username) { + contributionsCollection { + totalCommitContributions + totalPullRequestReviewContributions + totalIssueContributions + totalPullRequestContributions + contributionCalendar { + totalContributions + } + } + } + organization(login: $org) { + repositories(first: 100, privacy: PUBLIC) { + nodes { + name + defaultBranchRef { + target { + ... on Commit { + history(author: { login: $username }) { + totalCount + } + } + } + } + pullRequests(first: 100, states: [OPEN, MERGED, CLOSED]) { + nodes { + author { + login + } + createdAt + } + } + issues(first: 100, states: [OPEN, CLOSED]) { + nodes { + author { + login + } + createdAt + } + } + } + } + } + } + ` + + const result = await graphqlWithAuth(query, { + username, + org: GITHUB_ORG, + }) + + const user = result.user + const org = result.organization + + const stats: ContributorStats = { + username, + totalCommits: + user?.contributionsCollection?.totalCommitContributions || 0, + totalPullRequests: + user?.contributionsCollection?.totalPullRequestContributions || 0, + totalIssues: user?.contributionsCollection?.totalIssueContributions || 0, + totalReviews: + user?.contributionsCollection?.totalPullRequestReviewContributions || 0, + firstContribution: null, + lastContribution: null, + repositories: [], + } + + // Process repository-specific data + if (org?.repositories?.nodes) { + for (const repo of org.repositories.nodes) { + const commits = repo.defaultBranchRef?.target?.history?.totalCount || 0 + const pullRequests = + repo.pullRequests?.nodes?.filter( + (pr) => pr.author?.login === username + ).length || 0 + const issues = + repo.issues?.nodes?.filter( + (issue) => issue.author?.login === username + ).length || 0 + + if (commits > 0 || pullRequests > 0 || issues > 0) { + stats.repositories.push({ + name: repo.name, + commits, + pullRequests, + issues, + reviews: 0, // Would need additional query for reviews + }) + } + } + } + + return stats + } catch (error) { + console.error(`Error fetching GraphQL stats for ${username}:`, error) + return { + username, + totalCommits: 0, + totalPullRequests: 0, + totalIssues: 0, + totalReviews: 0, + firstContribution: null, + lastContribution: null, + repositories: [], + } + } +} + +// Batch fetch stats for multiple contributors +export async function getBatchContributorStats( + usernames: string[] +): Promise { + const stats = await Promise.all( + usernames.map((username) => getContributorStats(username)) + ) + return stats +} +*/ diff --git a/src/utils/docs.ts b/src/utils/docs.ts index cded19f8a..d3b7b50bd 100644 --- a/src/utils/docs.ts +++ b/src/utils/docs.ts @@ -8,6 +8,12 @@ import { notFound } from '@tanstack/react-router' import { createServerFn } from '@tanstack/react-start' import { z } from 'zod' import { setHeader } from '@tanstack/react-start/server' +// import { +// getContributorStats, +// getContributorStatsForLibrary, +// getBatchContributorStats, +// type ContributorStats, +// } from '~/server/github' export const loadDocs = async ({ repo, @@ -127,3 +133,86 @@ export const fetchRepoDirectoryContents = createServerFn({ return githubContents }) + +// GitHub contribution stats server functions - commented out due to performance/accuracy concerns +/* +export const fetchContributorStats = createServerFn({ method: 'GET' }) + .validator(z.object({ username: z.string() })) + .handler(async ({ data: { username } }) => { + const stats = await getContributorStats(username) + + // Cache for 30 minutes on shared cache + // Revalidate in the background + setHeader('Cache-Control', 'public, max-age=0, must-revalidate') + setHeader( + 'CDN-Cache-Control', + 'max-age=1800, stale-while-revalidate=1800, durable' + ) + + return stats + }) + +export const fetchContributorStatsForLibrary = createServerFn({ method: 'GET' }) + .validator( + z.object({ + username: z.string(), + libraryRepo: z.string(), + }) + ) + .handler(async ({ data: { username, libraryRepo } }) => { + const stats = await getContributorStatsForLibrary(username, libraryRepo) + + // Cache for 30 minutes on shared cache + // Revalidate in the background + setHeader('Cache-Control', 'public, max-age=0, must-revalidate') + setHeader( + 'CDN-Cache-Control', + 'max-age=1800, stale-while-revalidate=1800, durable' + ) + + return stats + }) + +export const fetchBatchContributorStats = createServerFn({ method: 'GET' }) + .validator(z.object({ usernames: z.array(z.string()) })) + .handler(async ({ data: { usernames } }) => { + const stats = await getBatchContributorStats(usernames) + + // Cache for 30 minutes on shared cache + // Revalidate in the background + setHeader('Cache-Control', 'public, max-age=0, must-revalidate') + setHeader( + 'CDN-Cache-Control', + 'max-age=1800, stale-while-revalidate=1800, durable' + ) + + return stats + }) + +// Helper function to get stats for all maintainers +export const fetchAllMaintainerStats = createServerFn({ + method: 'GET', +}).handler(async () => { + try { + // Import maintainers data + const { allMaintainers } = await import('~/libraries/maintainers') + + const usernames = allMaintainers.map((maintainer) => maintainer.github) + const stats = await getBatchContributorStats(usernames) + + // Cache for 30 minutes on shared cache + // Revalidate in the background + setHeader('Cache-Control', 'public, max-age=0, must-revalidate') + setHeader( + 'CDN-Cache-Control', + 'max-age=1800, stale-while-revalidate=1800, durable' + ) + + return stats + } catch (error) { + console.error('Error fetching all maintainer stats:', error) + // Return empty array if there's an error + return [] + } +}) +*/