mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(site): perform /docs redirect client-side
This commit is contained in:
@@ -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<HTMLAnchorElement> {
|
||||
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 (
|
||||
<a {...props} href={href}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -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 */}
|
||||
<nav className="flex flex-col py-3">
|
||||
{navLinks.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={clsx(
|
||||
'text-lg px-6 py-3',
|
||||
link.matchPath && currentPath.startsWith(link.matchPath) ? 'underline' : 'intent:underline'
|
||||
)}
|
||||
aria-current={link.matchPath && currentPath.startsWith(link.matchPath) ? 'page' : undefined}
|
||||
>
|
||||
{link.label} {link.external ? <ArrowUpRight size="1em" /> : null}
|
||||
</a>
|
||||
))}
|
||||
{navLinks.map((link) => {
|
||||
const isActive = link.matchPath && currentPath.startsWith(link.matchPath);
|
||||
const className = clsx('text-lg px-6 py-3', isActive ? 'underline' : 'intent:underline');
|
||||
|
||||
if (link.href === '/docs') {
|
||||
return (
|
||||
<GetStartedLink key={link.href} className={className} aria-current={isActive ? 'page' : undefined}>
|
||||
{link.label}
|
||||
</GetStartedLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a key={link.href} href={link.href} className={className} aria-current={isActive ? 'page' : undefined}>
|
||||
{link.label} {link.external ? <ArrowUpRight size="1em" /> : null}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
<a href={GITHUB_REPO_URL} className="text-lg px-6 py-3 inline-flex items-center gap-1 intent:underline">
|
||||
GitHub <ArrowUpRight size="1em" />
|
||||
</a>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
---
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { ArrowUpRight } from 'lucide-react';
|
||||
import Discord from '@/components/icons/Discord';
|
||||
import GitHub from '@/components/icons/GitHub';
|
||||
import Search from '@/components/Search';
|
||||
import { DISCORD_INVITE_URL, GITHUB_REPO_URL } from '@/consts';
|
||||
import FilmGrain from '../FilmGrain';
|
||||
import GetStartedLink from './GetStartedLink';
|
||||
import MobileNav from './MobileNav';
|
||||
|
||||
export interface Props {
|
||||
@@ -25,6 +27,8 @@ const navLinks: NavLink[] = [
|
||||
];
|
||||
|
||||
const currentPath = Astro.url.pathname;
|
||||
const isDocsActive = currentPath.startsWith('/docs');
|
||||
const otherNavLinks = navLinks.filter((link) => link.href !== '/docs');
|
||||
const { class: className, dark = false } = Astro.props;
|
||||
---
|
||||
|
||||
@@ -47,8 +51,18 @@ const { class: className, dark = false } = Astro.props;
|
||||
|
||||
{/* Desktop nav links */}
|
||||
<div class="hidden sm:flex -mb-px pr-3">
|
||||
<GetStartedLink
|
||||
client:idle
|
||||
className={clsx(
|
||||
'h-full flex items-center px-3 border-b gap-1',
|
||||
isDocsActive ? 'border-current' : 'border-transparent intent:border-current',
|
||||
)}
|
||||
aria-current={isDocsActive ? 'page' : undefined}
|
||||
>
|
||||
Docs
|
||||
</GetStartedLink>
|
||||
{
|
||||
navLinks.map((link) => (
|
||||
otherNavLinks.map((link) => (
|
||||
<a
|
||||
href={link.href}
|
||||
class:list={[
|
||||
@@ -84,7 +98,7 @@ const { class: className, dark = false } = Astro.props;
|
||||
</div>
|
||||
|
||||
{/* Mobile nav */}
|
||||
<MobileNav client:idle navLinks={navLinks} currentPath={currentPath} dark={dark}>
|
||||
<!-- <MobileNav client:idle navLinks={navLinks} currentPath={currentPath} dark={dark}>
|
||||
<slot name="mobile-nav" />
|
||||
</MobileNav>
|
||||
</MobileNav> -->
|
||||
</nav>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user