Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Clean up old layout logic #158

Merged
merged 12 commits into from
Jan 28, 2024
206 changes: 188 additions & 18 deletions app/components/DocsLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { DocSearch } from '@docsearch/react'
import * as React from 'react'
import { CgClose, CgMenuLeft } from 'react-icons/cg'
import { FaArrowLeft, FaArrowRight, FaTimes } from 'react-icons/fa'
import { NavLink, useMatches } from '@remix-run/react'
import { last } from '~/utils/utils'
import {
FaArrowLeft,
FaArrowRight,
FaDiscord,
FaGithub,
FaTimes,
} from 'react-icons/fa'
import { NavLink, useMatches, useNavigate, useParams } from '@remix-run/react'
import { Carbon } from '~/components/Carbon'
import { LinkOrA } from '~/components/LinkOrA'
import { Search } from '~/components/Search'
Expand All @@ -12,27 +17,192 @@ import { useLocalStorage } from '~/utils/useLocalStorage'
import { DocsCalloutQueryGG } from '~/components/DocsCalloutQueryGG'
import { DocsCalloutBytes } from '~/components/DocsCalloutBytes'
import { DocsLogo } from '~/components/DocsLogo'
import type { DocsConfig } from '~/utils/config'
import { generatePath, last } from '~/utils/utils'
import type { AvailableOptions } from '~/components/Select'
import type { ConfigSchema, MenuItem } from '~/utils/config'

export function DocsLayout({
name,
version,
colorFrom,
colorTo,
textColor,
/**
* Use framework in URL path
* Otherwise use framework in localStorage if it exists for this project
* Otherwise fallback to react
*/
function useCurrentFramework(frameworks: AvailableOptions) {
const { framework: paramsFramework } = useParams()
const localStorageFramework = localStorage.getItem('framework')

return (
paramsFramework ||
(localStorageFramework && localStorageFramework in frameworks
? localStorageFramework
: 'react')
)
}

const useMenuConfig = ({
config,
children,
framework,
repo,
}: {
config: ConfigSchema
framework: string
repo: string
}) => {
const frameworkMenuItems =
config.frameworkMenus.find((d) => d.framework === framework)?.menuItems ??
[]

const localMenu: MenuItem = {
label: 'Menu',
children: [
{
label: 'Home',
to: '..',
},
{
label: (
<div className="flex items-center gap-2">
GitHub <FaGithub className="text-lg opacity-20" />
</div>
),
to: `https://github.com/${repo}`,
},
{
label: (
<div className="flex items-center gap-2">
Discord <FaDiscord className="text-lg opacity-20" />
</div>
),
to: 'https://tlinz.com/discord',
},
],
}

return [
localMenu,
// Merge the two menus together based on their group labels
...config.menu.map((d) => {
const match = frameworkMenuItems.find((d2) => d2.label === d.label)
return {
label: d.label,
children: [
...d.children.map((d) => ({ ...d, badge: 'core' })),
...(match?.children ?? []).map((d) => ({ ...d, badge: framework })),
],
}
}),
...frameworkMenuItems.filter(
(d) => !config.menu.find((dd) => dd.label === d.label)
),
].filter(Boolean)
}

const useFrameworkConfig = ({
framework,
frameworks,
}: {
framework: string
frameworks: AvailableOptions
}) => {
const matches = useMatches()
const match = matches[matches.length - 1]
const navigate = useNavigate()

const frameworkConfig = React.useMemo(() => {
return {
label: 'Framework',
selected: frameworks[framework] ? framework : 'react',
available: frameworks,
onSelect: (option: { label: string; value: string }) => {
const url = generatePath(match.id, {
...match.params,
framework: option.value,
})

localStorage.setItem('framework', option.value)

navigate(url)
},
}
}, [frameworks, framework, match, navigate])

return frameworkConfig
}

const useVersionConfig = ({
availableVersions,
}: {
availableVersions: string[]
}) => {
const matches = useMatches()
const match = matches[matches.length - 1]
const params = useParams()
const version = params.version!
const navigate = useNavigate()

const versionConfig = React.useMemo(() => {
const available = availableVersions.reduce(
(acc: AvailableOptions, version) => {
acc[version] = {
label: version,
value: version,
}
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, availableVersions])

return versionConfig
}

type DocsLayoutProps = {
name: string
version: string
colorFrom: string
colorTo: string
textColor: string
config: DocsConfig
config: ConfigSchema
frameworks: AvailableOptions
availableVersions: string[]
repo: string
children: React.ReactNode
}) {
const frameworkConfig = config.frameworkConfig
const versionConfig = config.versionConfig
}

export function DocsLayout({
name,
version,
colorFrom,
colorTo,
textColor,
config,
frameworks,
availableVersions,
repo,
children,
}: DocsLayoutProps) {
const framework = useCurrentFramework(frameworks)
const frameworkConfig = useFrameworkConfig({ framework, frameworks })
const versionConfig = useVersionConfig({ availableVersions })
const menuConfig = useMenuConfig({ config, framework, repo })

const matches = useMatches()
const lastMatch = last(matches)

Expand All @@ -41,8 +211,8 @@ export function DocsLayout({
const detailsRef = React.useRef<HTMLElement>(null!)

const flatMenu = React.useMemo(
() => config.menu.flatMap((d) => d.children),
[config.menu]
() => menuConfig.flatMap((d) => d.children),
[menuConfig]
)

const docsMatch = matches.find((d) => d.pathname.includes('/docs'))
Expand All @@ -58,7 +228,7 @@ export function DocsLayout({

const [showBytes, setShowBytes] = useLocalStorage('showBytes', true)

const menuItems = config.menu.map((group, i) => {
const menuItems = menuConfig.map((group, i) => {
return (
<div key={i}>
<div className="text-[.9em] uppercase font-black">{group.label}</div>
Expand Down
2 changes: 1 addition & 1 deletion app/components/MarkdownLink.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { HTMLProps } from 'react'
import { Link } from '@remix-run/react'
import type { HTMLProps } from 'react'

export function MarkdownLink(props: HTMLProps<HTMLAnchorElement>) {
if ((props as { href: string }).href?.startsWith('http')) {
return <a {...props} />

Check warning on line 6 in app/components/MarkdownLink.tsx

View workflow job for this annotation

GitHub Actions / PR

Anchors must have content and the content must be accessible by a screen reader
}

return <Link to={props.href!} {...props} ref={null} prefetch="intent" />

Check warning on line 9 in app/components/MarkdownLink.tsx

View workflow job for this annotation

GitHub Actions / PR

Anchors must have content and the content must be accessible by a screen reader
}
67 changes: 29 additions & 38 deletions app/components/RedirectVersionBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Link, useLocation } from '@remix-run/react'
import { Link } from '@remix-run/react'
import { useLocalStorage } from '~/utils/useLocalStorage'
import { useClientOnlyRender } from '~/utils/useClientOnlyRender'

export function RedirectVersionBanner(props: {
currentVersion: string
version: string
latestVersion: string
redirectUrl: string
}) {
const location = useLocation()
const { version, latestVersion, redirectUrl } = props

// After user clicks hide, do not show modal for a month, and then remind users that there is a new version!
const [showModal, setShowModal] = useLocalStorage(
Expand All @@ -15,44 +16,34 @@ export function RedirectVersionBanner(props: {
1000 * 60 * 24 * 30
)

const isLowerVersion =
Number(props.currentVersion) < Number(props.latestVersion[1])
const redirectTarget = location.pathname.replace(
`v${props.currentVersion}`,
'latest'
)

if (!useClientOnlyRender()) {
return null
}

return (
<>
{isLowerVersion && showModal ? (
<div className="p-4 bg-blue-500 text-white flex items-center justify-center gap-4">
<div>
You are currently reading <strong>v{props.currentVersion}</strong>{' '}
docs. Redirect to{' '}
<a href={redirectTarget} className="font-bold underline">
latest
</a>{' '}
version?
</div>
<Link
to={redirectTarget}
replace
className="bg-white text-black py-1 px-2 rounded-md uppercase font-black text-xs"
>
Latest
</Link>
<button
onClick={() => setShowModal(false)}
className="bg-white text-black py-1 px-2 rounded-md uppercase font-black text-xs"
>
Hide
</button>
if (![latestVersion, 'latest'].includes(version) && showModal) {
return (
<div className="p-4 bg-blue-500 text-white flex items-center justify-center gap-4">
<div>
You are currently reading <strong>{version}</strong> docs. Redirect to{' '}
<a href={redirectUrl} className="font-bold underline">
latest
</a>{' '}
version?
</div>
) : null}
</>
)
<Link
to={redirectUrl}
replace
className="bg-white text-black py-1 px-2 rounded-md uppercase font-black text-xs"
>
Latest
</Link>
<button
onClick={() => setShowModal(false)}
className="bg-white text-black py-1 px-2 rounded-md uppercase font-black text-xs"
>
Hide
</button>
</div>
)
}
}
27 changes: 27 additions & 0 deletions app/projects/form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import reactLogo from '~/images/react-logo.svg'
import solidLogo from '~/images/solid-logo.svg'
import vueLogo from '~/images/vue-logo.svg'

export const repo = 'tanstack/form'

export const latestBranch = 'main'
export const latestVersion = 'v0'
export const availableVersions = ['v0']

export const colorFrom = 'from-yellow-500'
export const colorTo = 'to-yellow-600'
export const textColor = 'text-yellow-600'

export const frameworks = {
react: { label: 'React', logo: reactLogo, value: 'react' },
solid: { label: 'Solid', logo: solidLogo, value: 'solid' },
vue: { label: 'Vue', logo: vueLogo, value: 'vue' },
} as const

export type Framework = keyof typeof frameworks

export function getBranch(argVersion?: string) {
const version = argVersion || latestVersion

return ['latest', latestVersion].includes(version) ? latestBranch : version
}
Loading
Loading