+ <>
+
+
-
{library.name} Maintainers and Contributors
-
-
-
- {libraryContributors.map((maintainer) => (
-
- ))}
+
+
{library.name} Maintainers and Contributors
+
+
+ {/* View Mode Toggle */}
+
+
+ setViewMode('compact')}
+ className={`p-2 rounded-l-lg transition-colors ${
+ viewMode === 'compact'
+ ? 'bg-blue-500 text-white'
+ : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
+ }`}
+ title="Compact cards"
+ >
+
+
+ setViewMode('full')}
+ className={`p-2 transition-colors ${
+ viewMode === 'full'
+ ? 'bg-blue-500 text-white'
+ : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
+ }`}
+ title="Full cards"
+ >
+
+
+ setViewMode('row')}
+ className={`p-2 rounded-r-lg transition-colors ${
+ viewMode === 'row'
+ ? 'bg-blue-500 text-white'
+ : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
+ }`}
+ title="Row cards"
+ >
+
+
+
-
-
-
+
+
+ {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 */}
+
+ onViewModeChange('compact')}
+ className={`p-2 rounded-l-lg transition-colors ${
+ viewMode === 'compact'
+ ? 'bg-blue-500 text-white'
+ : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
+ }`}
+ title="Compact cards"
+ >
+
+
+ onViewModeChange('full')}
+ className={`p-2 transition-colors ${
+ viewMode === 'full'
+ ? 'bg-blue-500 text-white'
+ : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
+ }`}
+ title="Full cards"
+ >
+
+
+ onViewModeChange('row')}
+ className={`p-2 rounded-r-lg transition-colors ${
+ viewMode === 'row'
+ ? 'bg-blue-500 text-white'
+ : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
+ }`}
+ title="Row cards"
+ >
+
+
+
+
+ {/* Filter dropdown trigger */}
+
+
setIsOpen(!isOpen)}
+ className="inline-flex items-center gap-2 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
+ >
+
+ Filter & Sort
+ {hasFilters && (
+
+ {filterCount}
+
+ )}
+
+
+ {/* Dropdown menu */}
+ {isOpen && (
+
+
+
+
+ Filter & Sort Options
+
+ {hasFilters && (
+
+ Clear All
+
+ )}
+
+
+ {/* Group By */}
+
+
+ Group By
+
+
+ onGroupByChange(e.target.value as typeof groupBy)
+ }
+ className="w-full px-3 py-2 text-sm bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-gray-900 dark:text-gray-100"
+ >
+ No Grouping
+
+ Core vs Maintainers vs Contributors
+
+ By Library
+ By Role
+
+
+
+ {/* Sort By */}
+
+
+ Sort By
+
+
+ onSortByChange(e.target.value as typeof sortBy)
+ }
+ className="w-full px-3 py-2 text-sm bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-gray-900 dark:text-gray-100"
+ >
+ No Sorting
+ Name
+ Role/Level
+ Contributions
+
+
+
+ {/* Library filter */}
+
+
+ Filter by Libraries
+
+
+ {availableLibraries.map((library) => {
+ const isSelected =
+ selectedLibraries?.includes(library.id) || false
+
+ return (
+ toggleLibrary(library.id)}
+ className={`text-left px-3 py-2 rounded-md text-sm transition-colors ${
+ isSelected
+ ? `${
+ library.bgStyle ?? 'bg-gray-500'
+ } bg-opacity-100 text-white`
+ : `${
+ library.bgStyle ?? 'bg-gray-500'
+ } bg-opacity-20 dark:bg-opacity-30 text-green-900 dark:text-green-200 hover:bg-opacity-30 dark:hover:bg-opacity-40`
+ }`}
+ >
+ {library.name}
+
+ )
+ })}
+
+
+
+
+ )}
+
+
+ {/* Current filter chips */}
+
+ {selectedLibraries?.map((libraryId) => {
+ const library = availableLibraries.find(
+ (lib) => lib.id === libraryId
+ )
+ return (
+
+ {library?.name || libraryId}
+ toggleLibrary(libraryId)}
+ className="hover:bg-black hover:bg-opacity-10 dark:hover:bg-white dark:hover:bg-opacity-10 rounded p-0.5 transition-colors"
+ >
+
+
+
+ )
+ })}
+
+
+
+ )
+}
+
+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.
+
+
navigate({ search: {}, replace: true })}
+ className="inline-block px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors text-sm"
+ >
+ View All Maintainers
+
+
+
+ )}
+
+
+
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 (
-
-
-
+ <>
+
-
-
- {coreMaintainers.map((maintainer) => (
-
- ))}
-
-
+
+
+
-
-
- 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 */}
+
+
+ setViewMode('compact')}
+ className={`p-2 rounded-l-lg transition-colors ${
+ viewMode === 'compact'
+ ? 'bg-blue-500 text-white'
+ : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
+ }`}
+ title="Compact cards"
+ >
+
+
+ setViewMode('full')}
+ className={`p-2 transition-colors ${
+ viewMode === 'full'
+ ? 'bg-blue-500 text-white'
+ : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
+ }`}
+ title="Full cards"
+ >
+
+
+ setViewMode('row')}
+ className={`p-2 rounded-r-lg transition-colors ${
+ viewMode === 'row'
+ ? 'bg-blue-500 text-white'
+ : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
+ }`}
+ title="Row cards"
+ >
+
+
+
+
-
+
+
+
+ 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
-
+
+
+
-
+ >
)
}
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 []
+ }
+})
+*/