From 997f39b91574cb5113ec96c2ea46be43ce5a260e Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Tue, 2 Dec 2025 01:19:25 -0600 Subject: [PATCH 1/5] feat: improved frameworks page --- src/components/FrameworkCard.tsx | 135 ++++++++++++++++++ src/components/Markdown.tsx | 13 +- src/libraries/index.tsx | 2 + src/libraries/query.tsx | 1 + src/libraries/router.tsx | 1 + src/libraries/start.tsx | 1 + src/libraries/table.tsx | 1 + .../$version.docs.framework.index.tsx | 102 ++++++++++--- 8 files changed, 229 insertions(+), 27 deletions(-) create mode 100644 src/components/FrameworkCard.tsx diff --git a/src/components/FrameworkCard.tsx b/src/components/FrameworkCard.tsx new file mode 100644 index 000000000..908e35ec8 --- /dev/null +++ b/src/components/FrameworkCard.tsx @@ -0,0 +1,135 @@ +import { Link } from '@tanstack/react-router' +import { twMerge } from 'tailwind-merge' +import { FaCheck, FaCopy } from 'react-icons/fa' +import { getFrameworkOptions, Library } from '~/libraries' +import { useCopyButton } from '~/components/CopyMarkdownButton' +import { useToast } from '~/components/ToastProvider' + +export function FrameworkCard({ + framework, + libraryId, + version, + packageName, + index, + library, +}: { + framework: ReturnType[number] + libraryId: string + version: string + packageName: string + index: number + library: Library +}) { + const { notify } = useToast() + const [copied, onCopyClick] = useCopyButton(async () => { + await navigator.clipboard.writeText(packageName) + notify( +
+
Copied package name
+
+ {packageName} copied to clipboard +
+
+ ) + }) + + const hasCustomInstallPath = !!library.installPath + const installationPath = library.installPath + ? library.installPath + .replace('$framework', framework.value) + .replace('$libraryId', libraryId) + : 'installation' + + // Add framework hash fragment only for default installation pages (when installPath is not defined) + // Link component adds the # automatically, so we just pass the value without # + const installationHash = !hasCustomInstallPath ? framework.value : undefined + + return ( +
+ + {/* Framework Logo */} +
+ {framework.label} +
+ + {/* Framework Name */} +
+
+ {framework.label} +
+
+ + + {/* Package Name with Copy Button - Bottom of Card */} +
+
+ + {packageName} + + +
+ + Full install instructions + +
+ + {/* Accent indicator */} +
+
+ ) +} diff --git a/src/components/Markdown.tsx b/src/components/Markdown.tsx index f6d58ae5e..76d38ca46 100644 --- a/src/components/Markdown.tsx +++ b/src/components/Markdown.tsx @@ -30,7 +30,10 @@ const CustomHeading = ({ // Convert children to array and strip any inner anchor (native 'a' or MarkdownLink) const childrenArray = React.Children.toArray(children) const sanitizedChildren = childrenArray.map((child) => { - if (React.isValidElement(child) && (child.type === 'a' || child.type === MarkdownLink)) { + if ( + React.isValidElement(child) && + (child.type === 'a' || child.type === MarkdownLink) + ) { // replace anchor child with its own children so outer anchor remains the only link return child.props.children ?? null } @@ -45,7 +48,10 @@ const CustomHeading = ({ if (id) { return ( - + {heading} ) @@ -55,8 +61,7 @@ const CustomHeading = ({ } const makeHeading = - (type: HeadingLevel) => - (props: HTMLProps) => + (type: HeadingLevel) => (props: HTMLProps) => ( void hideCodesandboxUrl?: true hideStackblitzUrl?: true diff --git a/src/libraries/query.tsx b/src/libraries/query.tsx index c9aa9b18c..d812a541e 100644 --- a/src/libraries/query.tsx +++ b/src/libraries/query.tsx @@ -32,6 +32,7 @@ export const queryProject = { frameworks: ['react', 'solid', 'vue', 'svelte', 'angular'], scarfId: '53afb586-3934-4624-a37a-e680c1528e17', defaultDocs: 'framework/react/overview', + installPath: 'framework/$framework/installation', handleRedirects: (href: string) => { handleRedirects( reactQueryV3List, diff --git a/src/libraries/router.tsx b/src/libraries/router.tsx index c9c6f4db6..76dd334d8 100644 --- a/src/libraries/router.tsx +++ b/src/libraries/router.tsx @@ -37,6 +37,7 @@ export const routerProject = { frameworks: ['react', 'solid'], scarfId: '3d14fff2-f326-4929-b5e1-6ecf953d24f4', defaultDocs: 'framework/react/overview', + installPath: 'framework/$framework/installation', hideCodesandboxUrl: true, showVercelUrl: false, showNetlifyUrl: true, diff --git a/src/libraries/start.tsx b/src/libraries/start.tsx index 063a4703e..95bd02e6d 100644 --- a/src/libraries/start.tsx +++ b/src/libraries/start.tsx @@ -33,6 +33,7 @@ export const startProject = { embedEditor: 'codesandbox', frameworks: ['react', 'solid'], defaultDocs: 'framework/react/overview', + installPath: 'framework/$framework/build-from-scratch', scarfId: 'b6e2134f-e805-401d-95c3-2a7765d49a3d', showNetlifyUrl: true, showCloudflareUrl: true, diff --git a/src/libraries/table.tsx b/src/libraries/table.tsx index e70d92cd1..ef282deb4 100644 --- a/src/libraries/table.tsx +++ b/src/libraries/table.tsx @@ -41,6 +41,7 @@ export const tableProject = { ], scarfId: 'dc8b39e1-3fe9-4f3a-8e56-d4e2cf420a9e', defaultDocs: 'introduction', + corePackageName: 'table-core', handleRedirects: (href) => { handleRedirects( reactTableV7List, diff --git a/src/routes/$libraryId/$version.docs.framework.index.tsx b/src/routes/$libraryId/$version.docs.framework.index.tsx index 06fe0e696..7a0f9d99a 100644 --- a/src/routes/$libraryId/$version.docs.framework.index.tsx +++ b/src/routes/$libraryId/$version.docs.framework.index.tsx @@ -1,15 +1,35 @@ -import { Link, createFileRoute } from '@tanstack/react-router' +import { createFileRoute } from '@tanstack/react-router' import { twMerge } from 'tailwind-merge' +import { FaDiscord, FaGithub } from 'react-icons/fa' import { DocContainer } from '~/components/DocContainer' import { DocTitle } from '~/components/DocTitle' import { getFrameworkOptions, getLibrary } from '~/libraries' +import { FrameworkCard } from '~/components/FrameworkCard' export const Route = createFileRoute('/$libraryId/$version/docs/framework/')({ component: RouteComponent, }) +function getPackageName( + frameworkValue: string, + libraryId: string, + library: ReturnType +): string { + if (frameworkValue === 'vanilla') { + // For vanilla, use corePackageName if provided, otherwise just libraryId + const coreName = library.corePackageName || libraryId + return `@tanstack/${coreName}` + } + // Special case: Angular Query uses experimental package + if (frameworkValue === 'angular' && libraryId === 'query') { + return `@tanstack/angular-query-experimental` + } + // For other frameworks, use {framework}-{libraryId} pattern (e.g., @tanstack/react-table) + return `@tanstack/${frameworkValue}-${libraryId}` +} + function RouteComponent() { - const { libraryId } = Route.useParams() + const { libraryId, version } = Route.useParams() const library = getLibrary(libraryId) const frameworks = getFrameworkOptions(library.frameworks) @@ -27,32 +47,68 @@ function RouteComponent() { Supported {library.name} Frameworks
-
+
+ + {/* Framework Cards Grid */}
-
    - {frameworks.map((framework) => ( -
  • - - {framework.label} - TanStack {framework.label}{' '} - {library.name.replace('TanStack ', '')} - -
  • - ))} -
+ {frameworks.map((framework, i) => { + const packageName = getPackageName( + framework.value, + libraryId, + library + ) + return ( + + ) + })}
+ + {/* Call to Action Message */} +
+
+

+ Want to add support for another framework? +

+

+ We'd love to help you create a framework adapter for{' '} + {library.name}. Join our community to discuss implementation + details and get support. +

+ +
+
+
From 0b6d87778ab10a80a4e56e1911584a202232f0e5 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Thu, 4 Dec 2025 23:43:15 -0600 Subject: [PATCH 2/5] prettier --- .../scheduled/refresh-stats-cache.ts | 15 +- .../scheduled/sync-github-releases.ts | 19 +- .../sync-github-releases-background.ts | 8 +- scripts/migrate-data.md | 13 +- scripts/test-db-connection.ts | 13 +- src/components/FeedEntry.tsx | 2 +- src/components/FeedFilters.tsx | 31 ++- src/components/FrameworkSelect.tsx | 6 +- src/components/OpenSourceStats.tsx | 46 ++-- src/components/PaginationControls.tsx | 11 +- src/components/Select.tsx | 7 +- src/components/TableComponents.tsx | 31 +-- src/components/admin/FeedEntryEditor.tsx | 5 +- src/components/admin/FeedSyncStatus.tsx | 4 +- src/db/schema.ts | 12 +- src/hooks/useCapabilities.ts | 1 - src/hooks/useNpmDownloadCounter.ts | 1 - src/libraries/frameworks.tsx | 15 +- src/queries/feed.ts | 1 - src/queries/stats.ts | 10 +- ...ersion.docs.framework.$framework.index.tsx | 6 +- src/routes/$libraryId/route.tsx | 4 +- src/routes/admin/github-stats.tsx | 206 +++++++++--------- src/routes/admin/index.tsx | 15 +- src/routes/admin/npm-stats.tsx | 3 +- src/routes/admin/route.tsx | 10 +- src/routes/auth/signout.tsx | 4 +- src/server/feed/sync-all.ts | 1 - src/utils/cookies.server.ts | 82 ++++--- src/utils/env.functions.ts | 8 +- src/utils/feed-manual.ts | 1 - src/utils/feedSchema.ts | 1 - src/utils/stats-db.server.ts | 3 +- 33 files changed, 330 insertions(+), 265 deletions(-) diff --git a/netlify/functions/scheduled/refresh-stats-cache.ts b/netlify/functions/scheduled/refresh-stats-cache.ts index 9e368f42d..1c444a649 100644 --- a/netlify/functions/scheduled/refresh-stats-cache.ts +++ b/netlify/functions/scheduled/refresh-stats-cache.ts @@ -29,14 +29,17 @@ export const handler: Handler = async ( // Invoke the background function const functionUrl = `${process.env.URL}/.netlify/functions/refresh-stats-cache-background` - console.log('[refresh-stats-cache] Invoking background function:', functionUrl) + console.log( + '[refresh-stats-cache] Invoking background function:', + functionUrl + ) // Fire and forget - background function will run independently const response = await fetch(functionUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${cronSecret}`, + Authorization: `Bearer ${cronSecret}`, }, body: JSON.stringify({ source: 'scheduled-function', @@ -46,10 +49,14 @@ export const handler: Handler = async ( if (!response.ok) { const errorText = await response.text() - throw new Error(`Background function invocation failed: ${response.status} - ${errorText}`) + throw new Error( + `Background function invocation failed: ${response.status} - ${errorText}` + ) } - console.log('[refresh-stats-cache] Background function invoked successfully') + console.log( + '[refresh-stats-cache] Background function invoked successfully' + ) return { statusCode: 202, // Accepted - processing in background diff --git a/netlify/functions/scheduled/sync-github-releases.ts b/netlify/functions/scheduled/sync-github-releases.ts index c14f4ae2f..65f53ec97 100644 --- a/netlify/functions/scheduled/sync-github-releases.ts +++ b/netlify/functions/scheduled/sync-github-releases.ts @@ -18,7 +18,9 @@ export const handler: Handler = async ( event: HandlerEvent, context: HandlerContext ) => { - console.log('[sync-github-releases] Triggering background GitHub release sync...') + console.log( + '[sync-github-releases] Triggering background GitHub release sync...' + ) try { const cronSecret = process.env.CRON_SECRET @@ -29,14 +31,17 @@ export const handler: Handler = async ( // Invoke the background function const functionUrl = `${process.env.URL}/.netlify/functions/sync-github-releases-background` - console.log('[sync-github-releases] Invoking background function:', functionUrl) + console.log( + '[sync-github-releases] Invoking background function:', + functionUrl + ) // Fire and forget - background function will run independently const response = await fetch(functionUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${cronSecret}`, + Authorization: `Bearer ${cronSecret}`, }, body: JSON.stringify({ source: 'scheduled-function', @@ -46,10 +51,14 @@ export const handler: Handler = async ( if (!response.ok) { const errorText = await response.text() - throw new Error(`Background function invocation failed: ${response.status} - ${errorText}`) + throw new Error( + `Background function invocation failed: ${response.status} - ${errorText}` + ) } - console.log('[sync-github-releases] Background function invoked successfully') + console.log( + '[sync-github-releases] Background function invoked successfully' + ) return { statusCode: 202, // Accepted - processing in background diff --git a/netlify/functions/sync-github-releases-background.ts b/netlify/functions/sync-github-releases-background.ts index ee0a7f1f2..02b7ff653 100644 --- a/netlify/functions/sync-github-releases-background.ts +++ b/netlify/functions/sync-github-releases-background.ts @@ -24,14 +24,18 @@ export const handler: Handler = async ( event: HandlerEvent, context: HandlerContext ) => { - console.log('[sync-github-releases-background] Starting GitHub release sync...') + console.log( + '[sync-github-releases-background] Starting GitHub release sync...' + ) // Check authentication const cronSecret = process.env.CRON_SECRET const authHeader = event.headers.authorization || event.headers.Authorization if (!cronSecret) { - console.error('[sync-github-releases-background] CRON_SECRET not configured') + console.error( + '[sync-github-releases-background] CRON_SECRET not configured' + ) return { statusCode: 500, body: JSON.stringify({ diff --git a/scripts/migrate-data.md b/scripts/migrate-data.md index dfe0ba053..59f4ec79c 100644 --- a/scripts/migrate-data.md +++ b/scripts/migrate-data.md @@ -7,6 +7,7 @@ This guide covers migrating data from Convex to PostgreSQL (Neon DB). ## Prerequisites 1. **Export Convex Data** + - Use Convex dashboard or CLI to export all tables - Export format: JSON or CSV - Tables to export: @@ -31,15 +32,18 @@ This guide covers migrating data from Convex to PostgreSQL (Neon DB). Create a transformation script that: 1. **Convert Convex IDs to UUIDs** + - Generate new UUIDs for each record - Maintain mapping for foreign key relationships 2. **Transform Timestamps** + - Convex: milliseconds (number) - PostgreSQL: `timestamp with time zone` (Date object) - Convert: `new Date(timestamp)` for milliseconds 3. **Transform Arrays** + - Convex: stored as arrays - PostgreSQL: array columns (already compatible) - Ensure proper formatting @@ -101,17 +105,19 @@ WHERE u.id IS NULL; ### Step 4: Post-Migration Tasks 1. **Set up indexes** (if not created by Drizzle): + ```sql -- GIN indexes for array columns - CREATE INDEX IF NOT EXISTS feed_entries_library_ids_gin_idx + CREATE INDEX IF NOT EXISTS feed_entries_library_ids_gin_idx ON feed_entries USING GIN (library_ids); - CREATE INDEX IF NOT EXISTS feed_entries_tags_gin_idx + CREATE INDEX IF NOT EXISTS feed_entries_tags_gin_idx ON feed_entries USING GIN (tags); - + -- Full-text search (see drizzle/migrations/README.md) ``` 2. **Verify functionality**: + - Test authentication flow - Test feed queries - Test admin interfaces @@ -138,4 +144,3 @@ If migration fails: - **entryId field**: The `feed_entries` table uses `entryId` (string) as the unique identifier, not `id` (UUID). Make sure to map Convex feed entry IDs to `entryId` field. - **Metadata**: JSONB fields should be preserved as-is from Convex - **Capabilities**: Array fields should be preserved as arrays - diff --git a/scripts/test-db-connection.ts b/scripts/test-db-connection.ts index 51eaf421c..7684b6132 100644 --- a/scripts/test-db-connection.ts +++ b/scripts/test-db-connection.ts @@ -33,9 +33,15 @@ async function testConnection() { // Test 3: Count records console.log('3. Counting records...') - const userCount = await db.select({ count: sql`count(*)` }).from(users) - const roleCount = await db.select({ count: sql`count(*)` }).from(roles) - const feedCount = await db.select({ count: sql`count(*)` }).from(feedEntries) + const userCount = await db + .select({ count: sql`count(*)` }) + .from(users) + const roleCount = await db + .select({ count: sql`count(*)` }) + .from(roles) + const feedCount = await db + .select({ count: sql`count(*)` }) + .from(feedEntries) console.log(`✓ Users: ${userCount[0]?.count ?? 0}`) console.log(`✓ Roles: ${roleCount[0]?.count ?? 0}`) @@ -57,4 +63,3 @@ async function testConnection() { } testConnection() - diff --git a/src/components/FeedEntry.tsx b/src/components/FeedEntry.tsx index 770bc80a5..9f0560761 100644 --- a/src/components/FeedEntry.tsx +++ b/src/components/FeedEntry.tsx @@ -253,7 +253,7 @@ export function FeedEntry({ className="text-xs font-semibold text-gray-900 dark:text-gray-100 truncate hover:text-blue-600 dark:hover:text-blue-400 transition-colors" > {entry.title} - + {/* Excerpt */} diff --git a/src/components/FeedFilters.tsx b/src/components/FeedFilters.tsx index 4a0506458..520fb6043 100644 --- a/src/components/FeedFilters.tsx +++ b/src/components/FeedFilters.tsx @@ -206,7 +206,6 @@ export function FeedFilters({ releaseLevelsDiffer || (includePrerelease !== undefined && includePrerelease !== true) - // Render filter content (shared between mobile and desktop) const renderFilterContent = () => ( <> @@ -216,9 +215,7 @@ export function FeedFilters({ - handleFeaturedChange(featured ? undefined : true) - } + onChange={() => handleFeaturedChange(featured ? undefined : true)} count={facetCounts?.featured} />
@@ -234,9 +231,7 @@ export function FeedFilters({ onSelectNone={() => { onFiltersChange({ releaseLevels: undefined }) }} - isAllSelected={ - selectedReleaseLevels?.length === RELEASE_LEVELS.length - } + isAllSelected={selectedReleaseLevels?.length === RELEASE_LEVELS.length} isSomeSelected={ selectedReleaseLevels !== undefined && selectedReleaseLevels.length > 0 && @@ -452,10 +447,7 @@ export function FeedFilters({ compact /> )} -
e.stopPropagation()} - className="flex-shrink-0" - > +
e.stopPropagation()} className="flex-shrink-0"> - +
) @@ -498,8 +487,16 @@ export function FeedFilters({ desktopHeader={desktopHeader} > {/* Search - Desktop */} -