diff --git a/src/components/BlogAuthorFilter.tsx b/src/components/BlogAuthorFilter.tsx new file mode 100644 index 000000000..8a8da1e41 --- /dev/null +++ b/src/components/BlogAuthorFilter.tsx @@ -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 ( +
+ + + + + + 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', + )} + > + All authors + {!activeAuthor ? ( + + {authors.length > 0 ? : null} + {authors.map((name) => { + const isSelected = activeAuthor === name + return ( + 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', + )} + > + + {name} + {isSelected ? ( + + ) + })} + + +
+ ) +} diff --git a/src/components/BlogCard.tsx b/src/components/BlogCard.tsx new file mode 100644 index 000000000..43a3dd91f --- /dev/null +++ b/src/components/BlogCard.tsx @@ -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 ( + + {blogLibraries.length ? ( +
+ {blogLibraries.map((blogLibrary) => ( +
+ {blogLibrary.name.replace('TanStack ', '')} +
+ ))} +
+ ) : null} + {headerImage ? ( +
+ +
+ ) : null} +
+
+
{title}
+
+ by {formatAuthors(authors)} + {published ? ( + + ) : null} +
+ {excerpt ? ( +

+ {excerpt} +

+ ) : null} +
+
+
+ Read More +
+
+
+
+ ) +} diff --git a/src/components/DocsLayout.tsx b/src/components/DocsLayout.tsx index 9335ddc29..84ef7a89c 100644 --- a/src/components/DocsLayout.tsx +++ b/src/components/DocsLayout.tsx @@ -438,6 +438,10 @@ const useMenuConfig = ({ label: 'Home', to: '..', }, + { + label: 'Blog', + to: '/$libraryId/$version/docs/blog', + }, ...(frameworks.length > 1 ? [ { diff --git a/src/components/shop/ui/Select.tsx b/src/components/shop/ui/Select.tsx index 50b63d00b..dc98ffcb1 100644 --- a/src/components/shop/ui/Select.tsx +++ b/src/components/shop/ui/Select.tsx @@ -94,12 +94,13 @@ export function ShopSelect({ } return ( -
+