diff --git a/site/src/components/NavBar/GetStartedLink.tsx b/site/src/components/NavBar/GetStartedLink.tsx new file mode 100644 index 00000000..5cd3d570 --- /dev/null +++ b/site/src/components/NavBar/GetStartedLink.tsx @@ -0,0 +1,22 @@ +import { useStore } from '@nanostores/react'; +import { currentFramework } from '@/stores/preferences'; +import { buildDocsUrl } from '@/utils/docs/routing'; +import { findFirstGuide } from '@/utils/docs/sidebar'; +import useIsHydrated from '@/utils/useIsHydrated'; + +export interface GetStartedLinkProps extends React.AnchorHTMLAttributes { + children?: React.ReactNode; +} + +export default function GetStartedLink({ children, ...props }: GetStartedLinkProps) { + const framework = useStore(currentFramework); + const isHydrated = useIsHydrated(); + + const href = isHydrated && framework ? buildDocsUrl(framework, findFirstGuide(framework)) : '/docs'; + + return ( + + {children} + + ); +} diff --git a/site/src/components/NavBar/MobileNav.tsx b/site/src/components/NavBar/MobileNav.tsx index 4d34a824..a2d92930 100644 --- a/site/src/components/NavBar/MobileNav.tsx +++ b/site/src/components/NavBar/MobileNav.tsx @@ -3,6 +3,7 @@ import clsx from 'clsx'; import { ArrowUpRight, Menu, X } from 'lucide-react'; import { DISCORD_INVITE_URL, GITHUB_REPO_URL } from '@/consts'; import FilmGrain from '../FilmGrain'; +import GetStartedLink from './GetStartedLink'; interface NavLink { href: string; @@ -74,19 +75,24 @@ export default function MobileNav({ navLinks, currentPath, dark = false, childre {/* Navigation links */} diff --git a/site/src/components/docs/PreferenceSync.tsx b/site/src/components/docs/PreferenceSync.tsx index 2bd0a62d..33e5ae9b 100644 --- a/site/src/components/docs/PreferenceSync.tsx +++ b/site/src/components/docs/PreferenceSync.tsx @@ -1,7 +1,6 @@ import { useStore } from '@nanostores/react'; import { useEffect } from 'react'; import { currentFramework } from '@/stores/preferences'; -import type { SupportedFramework } from '@/types/docs'; import { getFrameworkPreferenceClient, setFrameworkPreferenceClient } from '@/utils/docs/preferences'; /** @@ -20,16 +19,13 @@ export function PreferenceSync() { // Initialize store from cookies on mount useEffect(() => { - const prefs = getFrameworkPreferenceClient(); - if (prefs) { - currentFramework.set(prefs); - } + currentFramework.set(getFrameworkPreferenceClient()); }, []); // Sync store changes to cookies useEffect(() => { if (framework) { - setFrameworkPreferenceClient(framework as SupportedFramework); + setFrameworkPreferenceClient(framework); } }, [framework]); diff --git a/site/src/utils/docs/__tests__/preferences.test.ts b/site/src/utils/docs/__tests__/preferences.test.ts index 7b50b104..5242217b 100644 --- a/site/src/utils/docs/__tests__/preferences.test.ts +++ b/site/src/utils/docs/__tests__/preferences.test.ts @@ -1,8 +1,9 @@ import type { AstroCookies } from 'astro'; import { describe, expect, it, vi } from 'vitest'; -import { SUPPORTED_FRAMEWORKS } from '@/types/docs'; +import { DEFAULT_FRAMEWORK, SUPPORTED_FRAMEWORKS } from '@/types/docs'; import { FRAMEWORK_COOKIE, + getFrameworkPreferenceClient, getPreferencesServer, STYLE_STORAGE_KEY_PREFIX, setFrameworkPreferenceClient, @@ -76,6 +77,47 @@ describe('preferences utilities', () => { }); }); + describe('getFrameworkPreferenceClient', () => { + it('should return DEFAULT_FRAMEWORK when no cookie is set', () => { + Object.defineProperty(document, 'cookie', { + get: () => '', + configurable: true, + }); + + expect(getFrameworkPreferenceClient()).toBe(DEFAULT_FRAMEWORK); + }); + + it('should return the framework from cookie when valid', () => { + for (const framework of SUPPORTED_FRAMEWORKS) { + Object.defineProperty(document, 'cookie', { + get: () => `${FRAMEWORK_COOKIE}=${framework}`, + configurable: true, + }); + + expect(getFrameworkPreferenceClient()).toBe(framework); + } + }); + + it('should return DEFAULT_FRAMEWORK when cookie has invalid value', () => { + Object.defineProperty(document, 'cookie', { + get: () => `${FRAMEWORK_COOKIE}=invalid-framework`, + configurable: true, + }); + + expect(getFrameworkPreferenceClient()).toBe(DEFAULT_FRAMEWORK); + }); + + it('should return null when document is undefined (SSR)', () => { + const originalDocument = globalThis.document; + // @ts-expect-error Testing SSR scenario + globalThis.document = undefined; + + expect(getFrameworkPreferenceClient()).toBeNull(); + + globalThis.document = originalDocument; + }); + }); + describe('setFrameworkPreferenceClient', () => { it('should set framework cookie', () => { // Mock document.cookie diff --git a/site/src/utils/docs/preferences.ts b/site/src/utils/docs/preferences.ts index 98d0f100..e0f8b695 100644 --- a/site/src/utils/docs/preferences.ts +++ b/site/src/utils/docs/preferences.ts @@ -1,6 +1,6 @@ import type { AstroCookies } from 'astro'; import type { AnySupportedStyle, SupportedFramework, SupportedStyle } from '@/types/docs'; -import { isValidFramework, isValidStyleForFramework } from '@/types/docs'; +import { DEFAULT_FRAMEWORK, isValidFramework, isValidStyleForFramework } from '@/types/docs'; // Cookie name for framework (server-side redirects) export const FRAMEWORK_COOKIE = 'vjs_docs_framework'; @@ -48,7 +48,7 @@ export function getFrameworkPreferenceClient(): SupportedFramework | null { ); const framework = cookies[FRAMEWORK_COOKIE]; - return framework && isValidFramework(framework) ? framework : null; + return framework && isValidFramework(framework) ? framework : DEFAULT_FRAMEWORK; } export function setFrameworkPreferenceClient(framework: SupportedFramework): void {