diff --git a/app/projects/query.tsx b/app/projects/query.tsx index 05e33b0b..290a5ff0 100644 --- a/app/projects/query.tsx +++ b/app/projects/query.tsx @@ -1,37 +1,83 @@ +import { FaDiscord, FaGithub } from 'react-icons/fa' +import reactLogo from '~/images/react-logo.svg' +import solidLogo from '~/images/solid-logo.svg' +import vueLogo from '~/images/vue-logo.svg' +import svelteLogo from '~/images/svelte-logo.svg' +import angularLogo from '~/images/angular-logo.svg' +import { useDocsConfig } from '~/utils/config' +import { Link } from '@remix-run/react' +import type { ConfigSchema, MenuItem } from '~/utils/config' + +export const repo = 'tanstack/query' + +export const latestBranch = 'main' +export const latestVersion = 'v5' +export const availableVersions = ['v5', 'v4', 'v3'] + export const gradientText = 'inline-block text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-amber-500' -export const repo = 'tanstack/query' +export const frameworks = { + react: { label: 'React', logo: reactLogo, value: 'react' }, + solid: { label: 'Solid', logo: solidLogo, value: 'solid' }, + vue: { label: 'Vue', logo: vueLogo, value: 'vue' }, + svelte: { label: 'Svelte', logo: svelteLogo, value: 'svelte' }, + angular: { label: 'Angular', logo: angularLogo, value: 'angular' }, +} as const -const latestBranch = 'main' +export type Framework = keyof typeof frameworks -export const latestVersion = 'v5' +export const createLogo = (version?: string) => ( + <> + + TanStack + + + Query{' '} + + {version === 'latest' ? latestVersion : version} + + + +) -export const availableVersions = [ - { - name: 'v5', - branch: latestBranch, - }, - { - name: 'v4', - branch: 'v4', - }, - { - name: 'v3', - branch: 'v3', - }, -] as const +export const localMenu: MenuItem = { + label: 'Menu', + children: [ + { + label: 'Home', + to: '..', + }, + { + label: ( +
+ GitHub +
+ ), + to: `https://github.com/${repo}`, + }, + { + label: ( +
+ Discord +
+ ), + to: 'https://tlinz.com/discord', + }, + ], +} export function getBranch(argVersion?: string) { const version = argVersion || latestVersion - if (version === 'latest') { - return latestBranch - } - - return ( - availableVersions.find((v) => v.name === version)?.branch ?? latestBranch - ) + return ['latest', latestVersion].includes(version) ? latestBranch : version } -export type Framework = 'angular' | 'react' | 'svelte' | 'vue' | 'solid' +export const useQueryDocsConfig = (config: ConfigSchema) => { + return useDocsConfig({ + config, + frameworks, + localMenu, + availableVersions, + }) +} diff --git a/app/routes/query.$.tsx b/app/routes/query.$.tsx index 023f68f4..506a1fc7 100644 --- a/app/routes/query.$.tsx +++ b/app/routes/query.$.tsx @@ -4,7 +4,7 @@ import type { LoaderFunctionArgs } from '@remix-run/node' export const loader = (context: LoaderFunctionArgs) => { handleRedirectsFromV3(context) - return redirect(`/query/latest`, 301) + return redirect(`/query/latest`) } function handleRedirectsFromV3(context: LoaderFunctionArgs) { @@ -105,8 +105,7 @@ function handleRedirectsFromV3(context: LoaderFunctionArgs) { reactQueryv3List.forEach((item) => { if (url.pathname.startsWith(`/query/v3/${item.from}`)) { throw redirect( - `/query/latest/${item.to}?from=reactQueryV3&original=https://tanstack.com/query/v3/${item.to}`, - 301 + `/query/latest/${item.to}?from=reactQueryV3&original=https://tanstack.com/query/v3/${item.to}` ) } }) diff --git a/app/routes/query.$version.docs.$.tsx b/app/routes/query.$version.docs.$.tsx new file mode 100644 index 00000000..b738e1a3 --- /dev/null +++ b/app/routes/query.$version.docs.$.tsx @@ -0,0 +1,44 @@ +import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node' +import { repo, getBranch } from '~/projects/query' +import { DefaultErrorBoundary } from '~/components/DefaultErrorBoundary' +import { seo } from '~/utils/seo' +import { useLoaderData, useParams } from '@remix-run/react' +import { loadDocs } from '~/utils/docs' +import { Doc } from '~/components/Doc' + +export const loader = async (context: LoaderFunctionArgs) => { + const { '*': docsPath, version } = context.params + const { url } = context.request + + return loadDocs({ + repo, + branch: getBranch(version), + docPath: `docs/${docsPath}`, + currentPath: url, + redirectPath: url.replace(/\/docs.*/, '/docs/framework/react/overview'), + }) +} + +export const meta: MetaFunction = ({ data }) => { + return seo({ + title: `${data?.title} | TanStack Query Docs`, + description: data?.description, + }) +} + +export const ErrorBoundary = DefaultErrorBoundary + +export default function RouteDocs() { + const { title, content, filePath } = useLoaderData() + const { version } = useParams() + const branch = getBranch(version) + return ( + + ) +} diff --git a/app/routes/query.$version.docs.$framework.$.tsx b/app/routes/query.$version.docs.$framework.$.tsx deleted file mode 100644 index 7065bdf9..00000000 --- a/app/routes/query.$version.docs.$framework.$.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node' -import { json, redirect } from '@remix-run/node' -import { extractFrontMatter, fetchRepoFile } from '~/utils/documents.server' -import { repo, getBranch } from '~/projects/query' -import { DefaultErrorBoundary } from '~/components/DefaultErrorBoundary' -import { seo } from '~/utils/seo' -import removeMarkdown from 'remove-markdown' -import { useLoaderData, useParams } from '@remix-run/react' -import { Doc } from '~/components/Doc' - -export const loader = async (context: LoaderFunctionArgs) => { - const { '*': docsPath, framework, version } = context.params - const url = new URL(context.request.url) - - /* - `v3` has only React docs. See: - https://github.com/TanStack/query/blob/v3/docs/config.json#L9 - - The redirect we throw here for all frameworks that do not have - docs for TanStack Query v3 fixes this bug: - https://github.com/TanStack/tanstack.com/issues/85 - */ - if (version === 'v3' && framework && framework !== 'react') { - throw redirect(`/query/v3/docs/react/${docsPath}`) - } - - // When first path part after docs does not contain framework name, add `react` - if ( - !context.request.url.includes('/docs/angular') && - !context.request.url.includes('/docs/react') && - !context.request.url.includes('/docs/solid') && - !context.request.url.includes('/docs/vue') && - !context.request.url.includes('/docs/svelte') - ) { - throw redirect(context.request.url.replace(/\/docs\//, '/docs/react/')) - } - - // Redirect old `adapters` pages - const adaptersRedirects = [ - { from: 'docs/react/adapters/vue-query', to: 'docs/vue/overview' }, - { from: 'docs/react/adapters/solid-query', to: 'docs/solid/overview' }, - { from: 'docs/react/adapters/svelte-query', to: 'docs/svelte/overview' }, - ] - - adaptersRedirects.forEach((item) => { - if (url.pathname.startsWith(`/query/v4/${item.from}`)) { - throw redirect(`/query/latest/${item.to}`, 301) - } - }) - - if (!docsPath) { - throw new Error('Invalid docs path') - } - - const filePath = `docs/${framework}/${docsPath}.md` - - const branch = getBranch(version) - const file = await fetchRepoFile(repo, branch, filePath) - - if (!file) { - throw redirect( - context.request.url.replace(/\/docs.*/, `/docs/${framework}`) - ) - } - - const frontMatter = extractFrontMatter(file) - const description = removeMarkdown(frontMatter.excerpt ?? '') - - return json( - { - title: frontMatter.data?.title, - description, - filePath, - content: frontMatter.content, - }, - { - headers: { - 'Cache-Control': 's-maxage=1, stale-while-revalidate=300', - }, - } - ) -} - -export const meta: MetaFunction = ({ data }) => { - return seo({ - title: `${data?.title} | TanStack Query Docs`, - description: data?.description, - }) -} - -export const ErrorBoundary = DefaultErrorBoundary - -export default function RouteDocs() { - const { title, content, filePath } = useLoaderData() - const { version } = useParams() - const branch = getBranch(version) - - return ( - - ) -} diff --git a/app/routes/query.$version.docs.$framework._index.tsx b/app/routes/query.$version.docs.$framework._index.tsx deleted file mode 100644 index b2e67ee3..00000000 --- a/app/routes/query.$version.docs.$framework._index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { LoaderFunctionArgs } from '@remix-run/node' -import { redirect } from '@remix-run/node' - -export const loader = (context: LoaderFunctionArgs) => { - // When first path part after docs contain framework name, just redirect to overview - if ( - context.request.url.includes('/docs/react') || - context.request.url.includes('/docs/solid') || - context.request.url.includes('/docs/vue') || - context.request.url.includes('/docs/svelte') - ) { - throw redirect(context.request.url + '/overview') - } - // Otherwise it's an old react doc path, so add `react` after `docs` - throw redirect(context.request.url.replace(/\/docs\//, '/docs/react/')) -} diff --git a/app/routes/query.$version.docs.$framework.tsx b/app/routes/query.$version.docs.$framework.tsx deleted file mode 100644 index 69bf336e..00000000 --- a/app/routes/query.$version.docs.$framework.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import * as React from 'react' -import { FaDiscord, FaGithub } from 'react-icons/fa' -import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node' -import { - Link, - json, - useLoaderData, - useMatches, - useNavigate, -} from '@remix-run/react' -import { seo } from '~/utils/seo' -import type { DocsConfig } from '~/components/DocsLayout' -import { DocsLayout } from '~/components/DocsLayout' -import { QueryGGBanner } from '~/components/QueryGGBanner' -import { - availableVersions, - getBranch, - gradientText, - latestVersion, - repo, -} from '~/projects/query' -import reactLogo from '~/images/react-logo.svg' -import solidLogo from '~/images/solid-logo.svg' -import vueLogo from '~/images/vue-logo.svg' -import svelteLogo from '~/images/svelte-logo.svg' -import angularLogo from '~/images/angular-logo.svg' -import type { AvailableOptions } from '~/components/Select' -import { generatePath } from '~/utils/utils' -import { getTanstackDocsConfig, type MenuItem } from '~/utils/config' - -export const loader = async (context: LoaderFunctionArgs) => { - const { version, framework } = context.params - const branch = getBranch(version) - const tanstackDocsConfig = await getTanstackDocsConfig(repo, branch) - - return json({ - tanstackDocsConfig, - framework, - version, - }) -} - -const frameworks = { - react: { label: 'React', logo: reactLogo, value: 'react' }, - solid: { label: 'Solid', logo: solidLogo, value: 'solid' }, - vue: { label: 'Vue', logo: vueLogo, value: 'vue' }, - svelte: { label: 'Svelte', logo: svelteLogo, value: 'svelte' }, - angular: { label: 'Angular', logo: angularLogo, value: 'angular' }, -} - -const logo = (version?: string) => ( - <> - - TanStack - - - Query{' '} - - {version === 'latest' ? latestVersion : version} - - - -) - -const localMenu: MenuItem = { - label: 'Menu', - children: [ - { - label: 'Home', - to: '..', - }, - { - label: ( -
- GitHub -
- ), - to: `https://github.com/${repo}`, - }, - { - label: ( -
- Discord -
- ), - to: 'https://tlinz.com/discord', - }, - ], -} - -export const meta: MetaFunction = () => { - return seo({ - title: - 'TanStack Query Docs | React Query, Solid Query, Svelte Query, Vue Query', - }) -} - -export default function RouteFrameworkParam() { - const matches = useMatches() - const match = matches[matches.length - 1] - const navigate = useNavigate() - const { tanstackDocsConfig, version, framework } = - useLoaderData() - - let config = tanstackDocsConfig - - const docsConfig = React.useMemo(() => { - const frameworkMenu = config.frameworkMenus?.find( - (d) => d.framework === framework - ) - if (!frameworkMenu) return null - return { - ...config, - menu: [localMenu, ...(frameworkMenu?.menuItems || [])], - } as DocsConfig - }, [framework, config]) - - const frameworkConfig = React.useMemo(() => { - if (!config.frameworkMenus) { - return undefined - } - - const availableFrameworks = config.frameworkMenus.reduce( - (acc: AvailableOptions, menuEntry) => { - acc[menuEntry.framework as string] = - frameworks[menuEntry.framework as keyof typeof frameworks] - return acc - }, - { react: frameworks['react'] } - ) - - return { - label: 'Framework', - selected: framework, - available: availableFrameworks, - onSelect: (option: { label: string; value: string }) => { - const url = generatePath(match.id, { - ...match.params, - framework: option.value, - }) - navigate(url) - }, - } - }, [framework, match, navigate, config.frameworkMenus]) - - const versionConfig = React.useMemo(() => { - const available = availableVersions.reduce( - (acc: AvailableOptions, version) => { - acc[version.name] = { - label: version.name, - value: version.name, - } - return acc - }, - { - latest: { - label: 'Latest', - value: 'latest', - }, - } - ) - - return { - label: 'Version', - selected: version, - available, - onSelect: (option: { label: string; value: string }) => { - const url = generatePath(match.id, { - ...match.params, - version: option.value, - }) - navigate(url) - }, - } - }, [version, match, navigate]) - - return ( - <> - - - - ) -} diff --git a/app/routes/query.$version.docs._index.tsx b/app/routes/query.$version.docs._index.tsx index c24e00ba..87944c92 100644 --- a/app/routes/query.$version.docs._index.tsx +++ b/app/routes/query.$version.docs._index.tsx @@ -3,6 +3,6 @@ import { redirect } from '@remix-run/node' export const loader = (context: LoaderFunctionArgs) => { throw redirect( - context.request.url.replace(/\/docs.*/, '/docs/react/overview') + context.request.url.replace(/\/docs.*/, '/docs/framework/react/overview') ) } diff --git a/app/routes/query.$version.docs.framework.$framework.$.tsx b/app/routes/query.$version.docs.framework.$framework.$.tsx new file mode 100644 index 00000000..648a7b91 --- /dev/null +++ b/app/routes/query.$version.docs.framework.$framework.$.tsx @@ -0,0 +1,45 @@ +import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node' +import { repo, getBranch } from '~/projects/query' +import { DefaultErrorBoundary } from '~/components/DefaultErrorBoundary' +import { seo } from '~/utils/seo' +import { useLoaderData, useParams } from '@remix-run/react' +import { Doc } from '~/components/Doc' +import { loadDocs } from '~/utils/docs' + +export const loader = async (context: LoaderFunctionArgs) => { + const { '*': docsPath, framework, version } = context.params + const { url } = context.request + + return loadDocs({ + repo, + branch: getBranch(version), + docPath: `docs/framework/${framework}/${docsPath}`, + currentPath: url, + redirectPath: url.replace(/\/docs.*/, '/docs/framework/react/overview'), + }) +} + +export const meta: MetaFunction = ({ data }) => { + return seo({ + title: `${data?.title} | TanStack Query Docs`, + description: data?.description, + }) +} + +export const ErrorBoundary = DefaultErrorBoundary + +export default function RouteDocs() { + const { title, content, filePath } = useLoaderData() + const { version } = useParams() + const branch = getBranch(version) + + return ( + + ) +} diff --git a/app/routes/query.$version.docs.$framework.examples._index.tsx b/app/routes/query.$version.docs.framework.$framework._index.tsx similarity index 60% rename from app/routes/query.$version.docs.$framework.examples._index.tsx rename to app/routes/query.$version.docs.framework.$framework._index.tsx index 0fdcd8cc..87944c92 100644 --- a/app/routes/query.$version.docs.$framework.examples._index.tsx +++ b/app/routes/query.$version.docs.framework.$framework._index.tsx @@ -1,10 +1,8 @@ -import { redirect } from '@remix-run/node' import type { LoaderFunctionArgs } from '@remix-run/node' +import { redirect } from '@remix-run/node' export const loader = (context: LoaderFunctionArgs) => { - const { framework } = context.params - throw redirect( - context.request.url.replace('/examples/', `/examples/${framework}/basic`) + context.request.url.replace(/\/docs.*/, '/docs/framework/react/overview') ) } diff --git a/app/routes/query.$version.docs.$framework.examples.$.tsx b/app/routes/query.$version.docs.framework.$framework.examples.$.tsx similarity index 81% rename from app/routes/query.$version.docs.$framework.examples.$.tsx rename to app/routes/query.$version.docs.framework.$framework.examples.$.tsx index 72b729c8..180f5b7f 100644 --- a/app/routes/query.$version.docs.$framework.examples.$.tsx +++ b/app/routes/query.$version.docs.framework.$framework.examples.$.tsx @@ -9,30 +9,28 @@ import { capitalize, slugToTitle } from '~/utils/utils' import { FaExternalLinkAlt } from 'react-icons/fa' export const loader = async (context: LoaderFunctionArgs) => { - const { '*': examplePath } = context.params - const [kind, _name] = (examplePath ?? '').split('/') - const [name, search] = _name.split('?') + const { framework, '*': name } = context.params - return json({ kind, name, search: search ?? '' }) + return json({ framework, name }) } export const meta: MetaFunction = ({ data }) => { return seo({ - title: `${capitalize(data.kind)} Query ${slugToTitle( + title: `${capitalize(data.framework)} Query ${slugToTitle( data.name )} Example | TanStack Query Docs`, description: `An example showing how to implement ${slugToTitle( data.name - )} in ${capitalize(data.kind)} Query`, + )} in ${capitalize(data.framework)} Query`, }) } export default function RouteExamples() { - const { kind, name, search } = useLoaderData() + const { framework, name } = useLoaderData() const { version } = useParams() const branch = getBranch(version) - const examplePath = branch === 'v3' ? name : [kind, name].join('/') + const examplePath = [framework, name].join('/') const [isDark, setIsDark] = React.useState(true) @@ -41,10 +39,10 @@ export default function RouteExamples() { }, []) const githubUrl = `https://github.com/${repo}/tree/${branch}/examples/${examplePath}` - const stackBlitzUrl = `https://stackblitz.com/github/${repo}/tree/${branch}/examples/${examplePath}?${search}embed=1&theme=${ + const stackBlitzUrl = `https://stackblitz.com/github/${repo}/tree/${branch}/examples/${examplePath}?embed=1&theme=${ isDark ? 'dark' : 'light' }` - const codesandboxUrl = `https://codesandbox.io/s/github/${repo}/tree/${branch}/examples/${examplePath}?${search}embed=1&theme=${ + const codesandboxUrl = `https://codesandbox.io/s/github/${repo}/tree/${branch}/examples/${examplePath}?embed=1&theme=${ isDark ? 'dark' : 'light' }` @@ -53,7 +51,7 @@ export default function RouteExamples() {
- {capitalize(kind)} Example: {slugToTitle(name)} + {capitalize(framework)} Example: {slugToTitle(name)}
{ + throw redirect(context.request.url.replace('/examples/', `/examples/basic`)) +} diff --git a/app/routes/query.$version.docs.tsx b/app/routes/query.$version.docs.tsx index 633ee1a2..955f8468 100644 --- a/app/routes/query.$version.docs.tsx +++ b/app/routes/query.$version.docs.tsx @@ -1,5 +1,52 @@ -import { Outlet } from '@remix-run/react' +import * as React from 'react' +import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node' +import { json, useLoaderData } from '@remix-run/react' +import { seo } from '~/utils/seo' +import { DocsLayout } from '~/components/DocsLayout' +import { QueryGGBanner } from '~/components/QueryGGBanner' +import { + getBranch, + createLogo, + repo, + useQueryDocsConfig, +} from '~/projects/query' +import { getTanstackDocsConfig } from '~/utils/config' -export default function RouteDocsParam() { - return +export const loader = async (context: LoaderFunctionArgs) => { + const { version } = context.params + const branch = getBranch(version) + const tanstackDocsConfig = await getTanstackDocsConfig(repo, branch) + + return json({ + tanstackDocsConfig, + version, + }) +} + +export const meta: MetaFunction = () => { + return seo({ + title: + 'TanStack Query Docs | React Query, Solid Query, Svelte Query, Vue Query', + }) +} + +export default function RouteFrameworkParam() { + const { tanstackDocsConfig, version } = useLoaderData() + let config = useQueryDocsConfig(tanstackDocsConfig) + + return ( + <> + + + + ) } diff --git a/app/routes/query.$version.tsx b/app/routes/query.$version.tsx index 08bc691a..c4ab044d 100644 --- a/app/routes/query.$version.tsx +++ b/app/routes/query.$version.tsx @@ -2,6 +2,7 @@ import { Link, Outlet, useLocation, useSearchParams } from '@remix-run/react' import { DefaultErrorBoundary } from '~/components/DefaultErrorBoundary' import { latestVersion } from '~/projects/query' import { RedirectVersionBanner } from '~/components/RedirectVersionBanner' +import { useClientOnlyRender } from '~/utils/useClientOnlyRender' export const ErrorBoundary = DefaultErrorBoundary @@ -14,6 +15,10 @@ export default function RouteVersionParam() { const version = location.pathname.match(/\/query\/v(\d)/)?.[1] || '999' + if (!useClientOnlyRender()) { + return null + } + return ( <> {showV3Redirect ? (