Skip to content

Commit 5e792f2

Browse files
authored
Merge pull request #2155 from mateuszbartosik/RDoc-3446
RDoc-3446 Add sticky topbar with LanguageSwitcher
2 parents a90454e + dda337c commit 5e792f2

File tree

9 files changed

+281
-97
lines changed

9 files changed

+281
-97
lines changed

docusaurus.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type * as Preset from "@docusaurus/preset-classic";
44

55
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
66

7+
const isStartOnlyCurrentVersion = process.env.DOCUSAURUS_START_VERSION_ENV === 'current';
8+
79
const config: Config = {
810
title: "RavenDB Documentation",
911
tagline:
@@ -43,6 +45,7 @@ const config: Config = {
4345
path: "7.1"
4446
}
4547
},
48+
onlyIncludeVersions: isStartOnlyCurrentVersion ? ['current'] : undefined,
4649
//editUrl:
4750
// 'https://github.com/ravendb/docs/tree/main/'
4851
},

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"private": true,
55
"scripts": {
66
"docusaurus": "docusaurus",
7-
"start": "npm run generate-icon-types && docusaurus start",
7+
"start": "npm run generate-icon-types && docusaurus start --no-minify",
8+
"start:current": "set \"DOCUSAURUS_START_VERSION_ENV=current\" && npm run generate-icon-types && docusaurus start --no-minify",
89
"build": "npm run generate-icon-types && docusaurus build",
910
"swizzle": "docusaurus swizzle",
1011
"deploy": "docusaurus deploy",

src/components/DocsTopbar.tsx

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import React, { useState, useEffect } from "react";
2+
import clsx from "clsx";
3+
import LanguageSwitcher from "@site/src/components/LanguageSwitcher";
4+
import { type DocsLanguage } from "./LanguageStore";
5+
6+
type DocsTopbarProps = {
7+
title: string;
8+
supportedLanguages?: DocsLanguage[];
9+
};
10+
11+
export default function DocsTopbar({
12+
title,
13+
supportedLanguages,
14+
}: DocsTopbarProps) {
15+
const [isVisible, setIsVisible] = useState(false);
16+
const [isCollapsed, setIsCollapsed] = useState(false);
17+
18+
useEffect(() => {
19+
const handleScroll = () => {
20+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
21+
const scrollThreshold = 250;
22+
setIsVisible(scrollTop >= scrollThreshold);
23+
};
24+
25+
window.addEventListener("scroll", handleScroll);
26+
handleScroll();
27+
28+
return () => {
29+
window.removeEventListener("scroll", handleScroll);
30+
};
31+
}, []);
32+
33+
useEffect(() => {
34+
if (window.innerWidth < 768) {
35+
setIsCollapsed(true);
36+
}
37+
}, []);
38+
39+
if (!supportedLanguages || supportedLanguages.length === 0) {
40+
return null;
41+
}
42+
43+
return (
44+
<div
45+
className={clsx(
46+
"sticky top-[71.46px] z-30",
47+
"rounded-xl",
48+
"transition-all duration-50 ease-in-out",
49+
{
50+
"max-h-[60px] opacity-100": isVisible,
51+
"max-h-0 opacity-0": !isVisible,
52+
},
53+
)}
54+
>
55+
<div className="row">
56+
<div className="col min-[1640px]:!p-0">
57+
<div
58+
className={clsx(
59+
"w-full p-2",
60+
"flex justify-between flex-wrap items-center",
61+
"rounded-xl",
62+
"border border-black/10 dark:border-white/10",
63+
"backdrop-blur supports-[backdrop-filter]:bg-white/60 dark:supports-[backdrop-filter]:bg-[#1b1b1d]/40 bg-white/90 dark:bg-[#1b1b1d]/90",
64+
"shadow-xl/30",
65+
"transition-all duration-300 ease-in-out",
66+
{
67+
"gap-2": !isCollapsed,
68+
},
69+
)}
70+
>
71+
<div className="flex justify-between items-center gap-2 truncate">
72+
<div
73+
className="text-base font-medium truncate"
74+
title={title}
75+
>
76+
{title}
77+
</div>
78+
<button
79+
className={clsx(
80+
"md:hidden ms-auto",
81+
"p-1 rounded",
82+
"hover:bg-black/5 dark:hover:bg-white/5",
83+
"transition-colors",
84+
)}
85+
onClick={() => setIsCollapsed(!isCollapsed)}
86+
aria-label={
87+
isCollapsed
88+
? "Show language switcher"
89+
: "Hide language switcher"
90+
}
91+
>
92+
<svg
93+
className={clsx(
94+
"w-4 h-4",
95+
"transition-transform duration-200",
96+
{
97+
"rotate-180": isCollapsed,
98+
},
99+
)}
100+
fill="none"
101+
stroke="currentColor"
102+
viewBox="0 0 24 24"
103+
>
104+
<path
105+
strokeLinecap="round"
106+
strokeLinejoin="round"
107+
strokeWidth={2}
108+
d="M19 9l-7 7-7-7"
109+
/>
110+
</svg>
111+
</button>
112+
</div>
113+
<div className="hidden md:block">
114+
<LanguageSwitcher
115+
supportedLanguages={supportedLanguages}
116+
flush
117+
/>
118+
</div>
119+
<div
120+
className={clsx(
121+
"md:hidden",
122+
"transition-all duration-300 ease-in-out",
123+
{
124+
"max-h-0 opacity-0 overflow-hidden":
125+
isCollapsed,
126+
"opacity-100": !isCollapsed,
127+
},
128+
)}
129+
>
130+
<LanguageSwitcher
131+
supportedLanguages={supportedLanguages}
132+
flush
133+
/>
134+
</div>
135+
</div>
136+
</div>
137+
<div className="col col--3 lg:block"></div>
138+
</div>
139+
</div>
140+
);
141+
}

src/components/LanguageContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { ReactNode } from "react";
2-
import { useLanguage } from "./LanguageContext";
2+
import { useLanguage } from "./LanguageStore";
33

44
interface LanguageContentProps {
55
language: string;

src/components/LanguageContext.tsx

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/components/LanguageStore.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useSyncExternalStore, useCallback } from "react";
2+
3+
export type DocsLanguage = "csharp" | "java" | "python" | "php" | "nodejs";
4+
5+
const DEFAULT_LANGUAGE: DocsLanguage = "csharp";
6+
const LANGUAGE_STORAGE_KEY = "docs-language";
7+
8+
const getLanguageFromLocalStorage = (): DocsLanguage => {
9+
return (
10+
(window.localStorage.getItem(LANGUAGE_STORAGE_KEY) as DocsLanguage) ||
11+
DEFAULT_LANGUAGE
12+
);
13+
};
14+
15+
const subscribe = (callback: () => void): (() => void) => {
16+
window.addEventListener("storage", callback);
17+
return () => {
18+
window.removeEventListener("storage", callback);
19+
};
20+
};
21+
22+
export const useLanguage = (): {
23+
language: DocsLanguage;
24+
setLanguage: (newLanguage: DocsLanguage) => void;
25+
} => {
26+
const language = useSyncExternalStore(
27+
subscribe,
28+
getLanguageFromLocalStorage,
29+
);
30+
31+
const setLanguage = useCallback((newLanguage: DocsLanguage) => {
32+
window.localStorage.setItem(LANGUAGE_STORAGE_KEY, newLanguage);
33+
window.dispatchEvent(new window.Event("storage"));
34+
}, []);
35+
36+
return { language, setLanguage };
37+
};
Lines changed: 61 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,72 @@
11
import React, { useEffect } from "react";
2-
import { useLanguage } from "./LanguageContext";
2+
import { useLanguage, type DocsLanguage } from "./LanguageStore";
33
import clsx from "clsx";
44

5-
const languages = [
6-
{ label: "C#", value: "csharp", brand: "#9179E4" },
7-
{ label: "Java", value: "java", brand: "#f89820" },
8-
{ label: "Python", value: "python", brand: "#fbcb24" },
9-
{ label: "PHP", value: "php", brand: "#8993be" },
10-
{ label: "Node.JS", value: "nodejs", brand: "#3c873a" },
5+
interface LanguageOption {
6+
label: string;
7+
value: DocsLanguage;
8+
brand: string;
9+
}
10+
11+
const languageOptions: LanguageOption[] = [
12+
{ label: "C#", value: "csharp", brand: "#9179E4" },
13+
{ label: "Java", value: "java", brand: "#f89820" },
14+
{ label: "Python", value: "python", brand: "#fbcb24" },
15+
{ label: "PHP", value: "php", brand: "#8993be" },
16+
{ label: "Node.JS", value: "nodejs", brand: "#3c873a" },
1117
];
1218

1319
type LanguageSwitcherProps = {
14-
supportedLanguages: string[];
20+
supportedLanguages: DocsLanguage[];
21+
flush?: boolean;
1522
};
1623

1724
export default function LanguageSwitcher({
18-
supportedLanguages,
25+
supportedLanguages,
26+
flush = false,
1927
}: LanguageSwitcherProps) {
20-
const { language, setLanguage } = useLanguage();
21-
22-
useEffect(() => {
23-
if (!supportedLanguages.includes(language)) {
24-
setLanguage(supportedLanguages[0]);
25-
}
26-
}, [supportedLanguages, language, setLanguage]);
27-
28-
return (
29-
<div className="flex flex-wrap gap-2 mb-8">
30-
{languages
31-
.filter((lang) => supportedLanguages.includes(lang.value))
32-
.map((lang) => {
33-
const isActive = language === lang.value;
34-
35-
return (
36-
<button
37-
key={lang.value}
38-
type="button"
39-
onClick={() => setLanguage(lang.value)}
40-
className={clsx(
41-
"px-3 py-1.5 rounded-md border text-sm transition-colors cursor-pointer",
42-
"border-gray-300 text-gray-500 hover:bg-black/5 hover:border-gray-500 hover:text-gray-600",
43-
"dark:text-gray-300 dark:border-gray-600 dark:hover:text-gray-200 dark:hover:border-gray-400 dark:hover:bg-white/5",
44-
)}
45-
style={
46-
isActive
47-
? {
48-
backgroundColor: `${lang.brand}20`,
49-
color: lang.brand,
50-
borderColor: lang.brand,
51-
}
52-
: {}
53-
}
54-
>
55-
{lang.label}
56-
</button>
57-
);
58-
})}
59-
</div>
60-
);
28+
const { language, setLanguage } = useLanguage();
29+
30+
const isCurrentLanguageSupported = supportedLanguages.includes(language);
31+
const firstSupportedLanguage = supportedLanguages[0];
32+
33+
useEffect(() => {
34+
if (!isCurrentLanguageSupported) {
35+
setLanguage(firstSupportedLanguage);
36+
}
37+
}, [isCurrentLanguageSupported, firstSupportedLanguage, setLanguage]);
38+
39+
return (
40+
<div className={clsx("flex flex-wrap gap-2", { 'mb-8': !flush })}>
41+
{languageOptions
42+
.filter((lang) => supportedLanguages.includes(lang.value))
43+
.map((lang) => {
44+
const isActive = language === lang.value;
45+
46+
return (
47+
<button
48+
key={lang.value}
49+
type="button"
50+
onClick={() => setLanguage(lang.value)}
51+
className={clsx(
52+
"px-3 py-1.5 rounded-md border text-sm transition-colors cursor-pointer",
53+
"border-gray-300 text-gray-500 hover:bg-black/5 hover:border-gray-500 hover:text-gray-600",
54+
"dark:text-gray-300 dark:border-gray-600 dark:hover:text-gray-200 dark:hover:border-gray-400 dark:hover:bg-white/5",
55+
)}
56+
style={
57+
isActive
58+
? {
59+
backgroundColor: `${lang.brand}20`,
60+
color: lang.brand,
61+
borderColor: lang.brand,
62+
}
63+
: {}
64+
}
65+
>
66+
{lang.label}
67+
</button>
68+
);
69+
})}
70+
</div>
71+
);
6172
}

0 commit comments

Comments
 (0)