Skip to content

Commit e7a18da

Browse files
feat(home): browse-by-category cards + /stack/$category pages + stacked social dropdown (#935)
* feat(home): browse-by-category cards, /stack/$category pages, stacked social dropdown Cherry-picks the genuinely useful pieces from #931 onto main: - Add a "framework" library group containing Start and Router, separate from data-and-state, so the framework-tier libraries get their own category. - Replace the bulky Open Source Libraries grid on the home page with a condensed five-card "Browse the stack" section (one card per category, linking to a dedicated category page). - Add /stack/$category landing pages: header, "Where to start" top pick, the full list of libraries in the category, and category-tagged blog posts. Drops the editorial-style verdict block, criteria section, in-this-guide rail, and compare-across-the-stack rail. - Replace the inline 2x3 social icon strip in Navbar with a stacked three-icon trigger that opens a labeled dropdown of all six channels. * style: oxfmt tidy on Navbar and index * Fix docs webhook invalidation * Add friendly URL-aware 404 page
1 parent 7202180 commit e7a18da

20 files changed

Lines changed: 1193 additions & 185 deletions

scripts/sync-docs-webhooks.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ if (!dryRun && !webhookSecret) {
2929
)
3030
}
3131

32+
if (!dryRun && webhookSecret) {
33+
if (webhookSecret.length < 16 || /\s/.test(webhookSecret)) {
34+
throw new Error(
35+
'GITHUB_WEBHOOK_SECRET must be a raw secret value, not Netlify env:get output for a secret variable.',
36+
)
37+
}
38+
}
39+
3240
function runGh(args: Array<string>) {
3341
return execFileSync('gh', args, {
3442
cwd: process.cwd(),

src/components/Navbar.tsx

Lines changed: 87 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ import { SearchButton } from './SearchButton'
3333
import { libraries, SIDEBAR_LIBRARY_IDS, type LibrarySlim } from '~/libraries'
3434
import { useClickOutside } from '~/hooks/useClickOutside'
3535
import { GithubIcon } from '~/components/icons/GithubIcon'
36+
import {
37+
Dropdown,
38+
DropdownContent,
39+
DropdownItem,
40+
DropdownTrigger,
41+
} from '~/components/Dropdown'
3642
import { DiscordIcon } from '~/components/icons/DiscordIcon'
3743
import { InstagramIcon } from '~/components/icons/InstagramIcon'
3844
import { BSkyIcon } from '~/components/icons/BSkyIcon'
@@ -216,40 +222,7 @@ export function Navbar({ children }: { children: React.ReactNode }) {
216222
</Link>
217223
)
218224

219-
const socialLinks = (
220-
<div className="flex items-center [&_a]:p-1.5 [&_a]:opacity-50 [&_a:hover]:opacity-100 [&_a]:transition-opacity [&_svg]:text-sm">
221-
<a
222-
href="https://github.com/TanStack"
223-
aria-label="Follow TanStack on GitHub"
224-
>
225-
<GithubIcon />
226-
</a>
227-
<a href="https://x.com/tan_stack" aria-label="Follow TanStack on X.com">
228-
<BrandXIcon />
229-
</a>
230-
<a
231-
href="https://bsky.app/profile/tanstack.com"
232-
aria-label="Follow TanStack on Besky"
233-
>
234-
<BSkyIcon />
235-
</a>
236-
<a
237-
href="https://instagram.com/tan_stack"
238-
aria-label="Follow TanStack on Instagram"
239-
>
240-
<InstagramIcon />
241-
</a>
242-
<a
243-
href="https://youtube.com/@tan_stack"
244-
aria-label="Subscribe to TanStack on YouTube"
245-
>
246-
<YouTubeIcon />
247-
</a>
248-
<a href="https://tlinz.com/discord" aria-label="Join TanStack Discord">
249-
<DiscordIcon />
250-
</a>
251-
</div>
252-
)
225+
const socialLinks = <SocialStack />
253226

254227
const navbar = (
255228
<div
@@ -777,3 +750,83 @@ export function Navbar({ children }: { children: React.ReactNode }) {
777750
</>
778751
)
779752
}
753+
754+
const SOCIAL_LINKS = [
755+
{
756+
label: 'GitHub',
757+
href: 'https://github.com/TanStack',
758+
Icon: GithubIcon,
759+
},
760+
{
761+
label: 'Discord',
762+
href: 'https://tlinz.com/discord',
763+
Icon: DiscordIcon,
764+
},
765+
{
766+
label: 'YouTube',
767+
href: 'https://youtube.com/@tan_stack',
768+
Icon: YouTubeIcon,
769+
},
770+
{
771+
label: 'X (Twitter)',
772+
href: 'https://x.com/tan_stack',
773+
Icon: BrandXIcon,
774+
},
775+
{
776+
label: 'Bluesky',
777+
href: 'https://bsky.app/profile/tanstack.com',
778+
Icon: BSkyIcon,
779+
},
780+
{
781+
label: 'Instagram',
782+
href: 'https://instagram.com/tan_stack',
783+
Icon: InstagramIcon,
784+
},
785+
] as const
786+
787+
function SocialStack() {
788+
const stackTop = SOCIAL_LINKS.slice(0, 3)
789+
790+
return (
791+
<Dropdown>
792+
<DropdownTrigger>
793+
<button
794+
type="button"
795+
aria-label="TanStack social channels"
796+
title="Social channels"
797+
className="inline-flex h-9 items-center pl-1 pr-2"
798+
>
799+
<span className="relative inline-flex items-center">
800+
{stackTop.map(({ label, Icon }, i) => (
801+
<span
802+
key={label}
803+
className={twMerge(
804+
'inline-flex h-7 w-7 items-center justify-center rounded-full border border-gray-200 bg-white text-gray-600 shadow-sm transition-transform dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300',
805+
i > 0 && '-ml-3',
806+
)}
807+
style={{ zIndex: stackTop.length - i }}
808+
>
809+
<Icon className="h-3 w-3" />
810+
</span>
811+
))}
812+
</span>
813+
</button>
814+
</DropdownTrigger>
815+
<DropdownContent align="end" sideOffset={8} className="min-w-44">
816+
{SOCIAL_LINKS.map(({ label, href, Icon }) => (
817+
<DropdownItem key={href} asChild>
818+
<a
819+
href={href}
820+
target="_blank"
821+
rel="noopener noreferrer"
822+
aria-label={`TanStack on ${label}`}
823+
>
824+
<Icon className="h-3.5 w-3.5" />
825+
<span>{label}</span>
826+
</a>
827+
</DropdownItem>
828+
))}
829+
</DropdownContent>
830+
</Dropdown>
831+
)
832+
}

0 commit comments

Comments
 (0)