Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions src/components/BlogAuthorFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Check, ChevronsUpDown } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import {
Dropdown,
DropdownContent,
DropdownItem,
DropdownSeparator,
DropdownTrigger,
} from '~/components/Dropdown'
import { findMaintainerByAuthorName } from '~/utils/authors'
import authorFallbackAvatar from '~/images/author-fallback.svg'

function getAuthorAvatar(name: string): string {
return findMaintainerByAuthorName(name)?.avatar ?? authorFallbackAvatar
}

type BlogAuthorFilterProps = {
authors: string[]
selected: string | undefined
onSelect: (author: string | undefined) => void
className?: string
}

export function BlogAuthorFilter({
authors,
selected,
onSelect,
className,
}: BlogAuthorFilterProps) {
// Ignore URL values that don't correspond to a real author in the list.
const activeAuthor =
selected && authors.includes(selected) ? selected : undefined

return (
<div className={twMerge('w-full', className)}>
<Dropdown>
<DropdownTrigger>
<button
type="button"
className={twMerge(
'relative w-full flex items-center gap-2 rounded-md py-1.5 px-2 text-left text-sm cursor-pointer transition-colors',
'focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
activeAuthor
? 'bg-blue-50 dark:bg-blue-950/40 ring-1 ring-blue-500/50 text-blue-700 dark:text-blue-200'
: 'hover:bg-gray-500/10',
)}
>
<span
className={twMerge(
'flex items-center justify-center w-6 h-6 rounded border overflow-hidden flex-shrink-0',
activeAuthor ? 'border-blue-500/30' : 'border-gray-500/20',
)}
>
<img
height={20}
width={20}
src={
activeAuthor
? getAuthorAvatar(activeAuthor)
: authorFallbackAvatar
}
alt=""
className="w-full h-full object-cover"
/>
</span>
<span className="truncate font-medium flex-1">
{activeAuthor ?? 'All authors'}
</span>
<span className="flex items-center pr-1">
<ChevronsUpDown
className="h-4 w-4 opacity-40"
aria-hidden="true"
/>
</span>
</button>
</DropdownTrigger>
<DropdownContent
align="start"
className="max-h-80 overflow-auto min-w-[16rem]"
>
<DropdownItem
onSelect={() => onSelect(undefined)}
className={twMerge(
'pr-8 relative',
!activeAuthor
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-200 font-medium'
: 'font-normal',
)}
>
<span className="truncate">All authors</span>
{!activeAuthor ? (
<Check
className="h-4 w-4 absolute right-2 text-blue-600 dark:text-blue-300"
aria-hidden="true"
/>
) : null}
</DropdownItem>
{authors.length > 0 ? <DropdownSeparator /> : null}
{authors.map((name) => {
const isSelected = activeAuthor === name
return (
<DropdownItem
key={name}
onSelect={() => onSelect(name)}
className={twMerge(
'pr-8 pl-2 relative',
isSelected
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-200 font-medium'
: 'font-normal',
)}
>
<img
height={20}
width={20}
src={getAuthorAvatar(name)}
alt=""
className="w-5 h-5 rounded object-cover flex-shrink-0"
/>
<span className="truncate">{name}</span>
{isSelected ? (
<Check
className="h-4 w-4 absolute right-2 text-blue-600 dark:text-blue-300"
aria-hidden="true"
/>
) : null}
</DropdownItem>
)
})}
</DropdownContent>
</Dropdown>
</div>
)
}
86 changes: 86 additions & 0 deletions src/components/BlogCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Link } from '@tanstack/react-router'
import { Card } from '~/components/Card'
import {
formatAuthors,
formatPublishedDate,
getBlogLibraries,
} from '~/utils/blog'
import { getNetlifyImageUrl } from '~/utils/netlifyImage'

export type BlogCardPost = {
slug: string
title: string
published: string
excerpt: string
headerImage: string | undefined
authors: string[]
library: string | undefined
}

type BlogCardProps = {
post: BlogCardPost
showLibraryBadges?: boolean
}

export function BlogCard({ post, showLibraryBadges = true }: BlogCardProps) {
const { slug, title, published, excerpt, headerImage, authors, library } =
post
const blogLibraries = showLibraryBadges ? getBlogLibraries(library) : []

return (
<Card
as={Link}
to="/blog/$"
params={{ _splat: slug } as never}
className="relative flex flex-col justify-between overflow-hidden transition-all hover:shadow-sm hover:border-blue-500"
>
{blogLibraries.length ? (
<div className="absolute right-3 top-3 z-10 flex flex-wrap justify-end gap-1">
{blogLibraries.map((blogLibrary) => (
<div
key={blogLibrary.id}
className={`rounded-md px-2 py-1 text-xs font-black uppercase shadow-sm ${blogLibrary.bgStyle} ${blogLibrary.badgeTextStyle ?? 'text-white'}`}
>
{blogLibrary.name.replace('TanStack ', '')}
</div>
))}
</div>
) : null}
{headerImage ? (
<div className="aspect-video overflow-hidden bg-gray-100 dark:bg-gray-800">
<img
src={getNetlifyImageUrl(headerImage)}
alt=""
loading="lazy"
decoding="async"
className="w-full h-full object-cover"
/>
</div>
) : null}
<div className="p-4 md:p-8 flex flex-col gap-4 flex-1 justify-between">
<div>
<div className="text-lg font-extrabold">{title}</div>
<div className="text-xs italic font-light mt-1">
by {formatAuthors(authors)}
{published ? (
<time dateTime={published} title={formatPublishedDate(published)}>
{' '}
on {formatPublishedDate(published)}
</time>
) : null}
</div>
{excerpt ? (
<p className="text-sm mt-4 text-gray-600 dark:text-gray-400 leading-7 line-clamp-4">
{excerpt}
</p>
) : null}
</div>
<div>
<div className="text-blue-500 uppercase font-black text-sm">
Read More
</div>
</div>
</div>
</Card>
)
}
4 changes: 4 additions & 0 deletions src/components/DocsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,10 @@ const useMenuConfig = ({
label: 'Home',
to: '..',
},
{
label: 'Blog',
to: '/$libraryId/$version/docs/blog',
},
Comment on lines +441 to +444
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a relative path for the Blog menu item to keep nav indexing correct.

With to: '/$libraryId/$version/docs/blog', the item won’t match the relative path comparison used for prev/next derivation (Line 592), so navigation order can break on the blog docs page.

Proposed diff
       {
         label: 'Blog',
-        to: '/$libraryId/$version/docs/blog',
+        to: './blog',
       },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/DocsLayout.tsx` around lines 441 - 444, In DocsLayout update
the Blog nav item so its route is relative instead of absolute: replace the menu
entry with label 'Blog' that currently uses to: '/$libraryId/$version/docs/blog'
to a relative path (e.g. 'blog' or './blog') so it matches the relative path
comparison used by the prev/next derivation logic (the DocsLayout navigation
items and the code that computes prev/next). Ensure the amended 'Blog' menu item
uses the same relative format as other doc items so navigation ordering works
correctly.

...(frameworks.length > 1
? [
{
Expand Down
3 changes: 2 additions & 1 deletion src/components/shop/ui/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,13 @@ export function ShopSelect({
}

return (
<div className={twMerge('relative', className)} onKeyDown={onKeyDown}>
<div className={twMerge('relative', className)}>
<button
ref={triggerRef}
type="button"
aria-haspopup="listbox"
aria-expanded={open}
onKeyDown={onKeyDown}
onClick={() => {
setOpen((o) => !o)
if (!open) setFocused(value ?? options[0]?.value ?? null)
Expand Down
5 changes: 5 additions & 0 deletions src/images/author-fallback.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 11 additions & 5 deletions src/libraries/maintainers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ export const allMaintainers: Maintainer[] = [
'ranger',
'store',
'pacer',
'cli',
'mcp',
'react-charts',
],
maintainerOf: ['intent'],
frameworkExpertise: ['react', 'solid'],
specialties: ['Architecture', 'Core API', 'Documentation'],
social: {
Expand Down Expand Up @@ -71,7 +73,7 @@ export const allMaintainers: Maintainer[] = [
github: 'crutchcorn',
creatorOf: ['form'],
maintainerOf: ['store', 'config'],
frameworkExpertise: ['react', 'solid', 'vue', 'angular'],
frameworkExpertise: ['react', 'solid', 'vue', 'angular', 'lit'],
specialties: ['Forms', 'Validation', 'State Management'],
social: {
twitter: 'https://x.com/crutchcorn',
Expand All @@ -98,14 +100,15 @@ export const allMaintainers: Maintainer[] = [
avatar: 'https://github.com/kevinvandy.png',
github: 'kevinvandy',
creatorOf: ['pacer', 'hotkeys'],
maintainerOf: ['table'],
maintainerOf: ['table', 'store'],
contributorOf: ['virtual'],
consultantOf: ['query'],
frameworkExpertise: ['react', 'preact', 'solid'],
specialties: ['Tables', 'Data Grids', 'Dashboards'],
specialties: ['Data Grids', 'Performance', 'Dashboards'],
social: {
twitter: 'https://x.com/kevinvancott',
bluesky: 'https://bsky.app/profile/kevinvancott.dev',
linkedIn: 'https://www.linkedin.com/in/kevinthomasvancott',
website: 'https://kevinvancott.dev',
},
workshopsAvailable: true,
Expand Down Expand Up @@ -135,7 +138,7 @@ export const allMaintainers: Maintainer[] = [
},
},
{
name: 'Chris Horobin',
name: 'Christopher Horobin',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep backward compatibility for renamed author display names.

Renaming Chris Horobin to Christopher Horobin can break exact-name matching for existing post metadata, causing fallback avatars/filter mismatches until all historical author strings are migrated. Consider keeping the old canonical value here until content is migrated, or add alias support in author resolution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libraries/maintainers.ts` at line 141, The maintainer entry's canonical
name was changed to "Christopher Horobin" which can break exact-match lookups;
restore backward compatibility by keeping the original canonical value "Chris
Horobin" in the name field in src/libraries/maintainers.ts and add either a new
displayName: "Christopher Horobin" or an aliases: ["Christopher Horobin"]
property for that maintainer, or alternatively implement alias support in the
author resolution logic (e.g., the getAuthorByName/resolveAuthor function) to
match against aliases/displayName when resolving authors.

isCoreMaintainer: true,
avatar: 'https://github.com/chorobin.png',
github: 'chorobin',
Expand All @@ -161,12 +164,13 @@ export const allMaintainers: Maintainer[] = [
isCoreMaintainer: true,
avatar: 'https://github.com/KyleAMathews.png',
github: 'KyleAMathews',
creatorOf: ['db'],
creatorOf: ['db', 'intent'],
frameworkExpertise: ['react'],
specialties: ['Sync Engines'],
},
{
name: 'Alem Tuzlak',
isCoreMaintainer: true,
avatar: 'https://github.com/AlemTuzlak.png',
github: 'AlemTuzlak',
creatorOf: ['ai', 'devtools'],
Expand Down Expand Up @@ -304,6 +308,7 @@ export const allMaintainers: Maintainer[] = [
name: 'Sarah Gerrard',
avatar: 'https://github.com/ladybluenotes.png',
github: 'ladybluenotes',
creatorOf: ['intent'],
contributorOf: [
'ai',
'config',
Expand Down Expand Up @@ -334,6 +339,7 @@ export const allMaintainers: Maintainer[] = [
'https://cdn.bsky.app/img/avatar/plain/did:plc:gtnigsmgu7jyrc4tnkvn62qw/bafkreiceysbj4o6jrbbniudtwj3tcsns6rvwcxyjsqiaumeojurwbkki5a@jpeg',
github: 'riccardoperra',
maintainerOf: ['table'],
contributorOf: ['devtools', 'hotkeys', 'pacer'],
frameworkExpertise: ['angular', 'solid'],
specialties: [],
social: {
Expand Down
Loading
Loading