Skip to content
24 changes: 24 additions & 0 deletions src/components/HashRedirect.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'

/**
* Client-side redirect for fragment links to sections that have moved to other
* pages. `next.config` redirects can't act on the URL hash (it never reaches the
* server), so this catches old deep links like
* `/selfhosted/troubleshooting#debugging-turn-connections` on mount and forwards
* them to the new location.
*
* @param {Record<string, string>} map - old anchor id -> new path (optionally with #anchor)
*/
export function HashRedirect({ map = {} }) {
const router = useRouter()
useEffect(() => {
const id = window.location.hash.replace(/^#/, '')
if (id && map[id]) {
router.replace(map[id])
}
// run once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return null
}
140 changes: 132 additions & 8 deletions src/components/NavigationDocs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,28 @@ export const docsNavigation = [
],
},
{ title: 'Advanced Guide', href: '/selfhosted/selfhosted-guide' },
{ title: 'Troubleshooting', href: '/selfhosted/troubleshooting' },
{
title: 'Troubleshooting',
isOpen: false,
links: [
{ title: 'Overview', href: '/selfhosted/troubleshooting' },
{ title: 'Installation', href: '/selfhosted/troubleshooting/installation' },
{
title: 'Embedded IdP',
href: '/selfhosted/troubleshooting/identity-provider',
},
{ title: 'Dashboard', href: '/selfhosted/troubleshooting/dashboard' },
{
title: 'Certificates',
href: '/selfhosted/troubleshooting/certificates',
},
{
title: 'Connectivity',
href: '/selfhosted/troubleshooting/connectivity',
},
{ title: 'Database', href: '/selfhosted/troubleshooting/database' },
],
},
{
title: 'Migration Guides',
isOpen: false,
Expand Down Expand Up @@ -856,14 +877,117 @@ export const docsNavigation = [
title: 'GET MORE HELP',
links: [
{
title: 'Troubleshooting client issues',
href: '/help/troubleshooting-client',
},
{
title: 'Troubleshooting relayed connections',
href: '/help/troubleshooting-relayed-connections',
title: 'Troubleshooting',
isOpen: false,
links: [
{ title: 'Overview', href: '/help/troubleshooting' },
{
title: 'NetBird Client',
isOpen: false,
links: [
{ title: 'Overview', href: '/help/troubleshooting-client' },
{ title: 'Linux', href: '/help/troubleshooting-client/linux' },
{ title: 'Windows', href: '/help/troubleshooting-client/windows' },
{ title: 'macOS', href: '/help/troubleshooting-client/macos' },
{ title: 'Android', href: '/help/troubleshooting-client/android' },
{ title: 'iOS', href: '/help/troubleshooting-client/ios' },
],
},
{
title: 'Self-hosted',
isOpen: false,
links: [
{ title: 'Overview', href: '/selfhosted/troubleshooting' },
{
title: 'Installation',
href: '/selfhosted/troubleshooting/installation',
},
{
title: 'Embedded IdP',
href: '/selfhosted/troubleshooting/identity-provider',
},
{
title: 'Dashboard',
href: '/selfhosted/troubleshooting/dashboard',
},
{
title: 'Certificates',
href: '/selfhosted/troubleshooting/certificates',
},
{
title: 'Connectivity',
href: '/selfhosted/troubleshooting/connectivity',
},
{
title: 'Database',
href: '/selfhosted/troubleshooting/database',
},
],
},
{
title: 'Cloud & identity',
isOpen: false,
links: [
{
title: 'Pending approval',
href: '/help/troubleshooting-account-access',
},
{ title: 'IdP & SSO setup', href: '/manage/team/single-sign-on' },
{ title: 'User provisioning', href: '/manage/team/idp-sync' },
{
title: 'Plan limits & quotas',
href: '/manage/settings/plans-and-billing',
},
],
},
{
title: 'Connectivity',
isOpen: false,
links: [
{
title: 'Relayed Connections',
href: '/help/troubleshooting-relayed-connections',
},
{
title: 'Resource Connectivity',
href: '/help/troubleshooting-resource-connectivity',
},
{
title: 'NAT & Connectivity',
href: '/about-netbird/understanding-nat-and-connectivity',
},
{
title: 'Ports & Firewalls',
href: '/about-netbird/ports-and-firewalls',
},
],
},
{
title: 'Access control',
isOpen: false,
links: [
{ title: 'Policies', href: '/manage/access-control' },
{
title: 'Posture checks',
href: '/manage/access-control/posture-checks',
},
{
title: 'Groups',
href: '/manage/access-control#understanding-groups',
},
],
},
{
title: 'Report a bug',
isOpen: false,
links: [
{ title: 'Overview', href: '/help/report-bug-issues' },
{ title: 'Community Support', href: '/help/community-support' },
{ title: 'NetBird Support', href: '/help/netbird-support' },
],
},
],
},
{ title: 'Report bugs and issues', href: '/help/report-bug-issues' },
{
title: 'Support Matrix',
isOpen: false,
Expand Down
49 changes: 49 additions & 0 deletions src/components/StillStuck.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Fragment } from 'react'
import { Button } from '@/components/Button'

/**
* "Still stuck?" call-to-action banner for the bottom of the Troubleshooting
* hub. Renders a short prompt and a row of action buttons.
*
* @param {string} [title='Still stuck?']
* @param {string} description
* @param {Array<{label: string, href: string, primary?: boolean}>} actions
* @param {string} [separator] - Optional word rendered between buttons (e.g. "or"),
* so two equally-weighted actions don't read as a recommended-vs-secondary pair.
*/
export function StillStuck({
title = 'Still stuck?',
description,
actions = [],
separator,
}) {
return (
<div className="not-prose my-12 rounded-2xl bg-zinc-50 p-6 ring-1 ring-inset ring-zinc-900/7.5 dark:bg-white/2.5 dark:ring-white/10">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-white">
{title}
</h2>
{description && (
<p className="mt-1 max-w-2xl text-sm text-zinc-600 dark:text-zinc-400">
{description}
</p>
)}
<div className="mt-5 flex flex-wrap items-center gap-3">
{actions.map((action, i) => (
<Fragment key={action.href}>
{i > 0 && separator && (
<span className="text-sm text-zinc-500 dark:text-zinc-400">
{separator}
</span>
)}
<Button
href={action.href}
variant={action.primary ? 'primary' : 'secondary'}
>
{action.label}
</Button>
</Fragment>
))}
</div>
</div>
)
}
78 changes: 78 additions & 0 deletions src/components/SupportBanner.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Audience banner for the "Report bugs and issues" page. Two tones:
* - "community": neutral, for everyone outside the managed Cloud.
* - "support": orange/netbird accent, for paying NetBird Cloud customers.
*
* @param {'community'|'support'} [tone='community']
* @param {string} [id] - Optional id so the banner can be a scroll target
* @param {string} title
* @param {string} [badge] - Small pill (e.g. "Free", "Cloud customers")
* @param {string} description
* @param {Array<{label: string, href: string}>} [links] - Bullet links to the
* section's channels / anchors.
*/
export function SupportBanner({
tone = 'community',
id,
title,
badge,
description,
links = [],
}) {
const isSupport = tone === 'support'
const accent = isSupport
? 'border-[#F28C28]'
: 'border-zinc-300 dark:border-zinc-600'
const badgeClass = isSupport
? 'bg-[#F28C28]/10 text-[#C2410C] ring-[#F28C28]/30 dark:text-[#FFAC1C] dark:ring-[#F28C28]/40'
: 'bg-zinc-900/5 text-zinc-600 ring-zinc-900/10 dark:bg-white/5 dark:text-zinc-400 dark:ring-white/10'

return (
<div
id={id}
className={`not-prose my-6 scroll-mt-24 rounded-2xl border-l-4 ${accent} bg-zinc-50 p-5 ring-1 ring-inset ring-zinc-900/7.5 dark:bg-white/2.5 dark:ring-white/10`}
>
{(title || badge) && (
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
{title && (
<p className="text-lg font-semibold text-zinc-900 dark:text-white">
{title}
</p>
)}
{badge && (
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ring-1 ring-inset ${badgeClass}`}
>
{badge}
</span>
)}
</div>
)}
{description && (
<p className="mt-1 max-w-3xl text-sm text-zinc-600 dark:text-zinc-400">
{description}
</p>
)}
{links.length > 0 && (
<ul className="mt-3 space-y-1">
{links.map((link) => (
<li key={link.href} className="flex items-start gap-x-2 text-sm">
<span
aria-hidden="true"
className={isSupport ? 'text-[#F28C28]' : 'text-zinc-400 dark:text-zinc-500'}
>
</span>
<a
href={link.href}
className="text-zinc-700 underline-offset-2 transition hover:text-[#F28C28] hover:underline dark:text-zinc-300 dark:hover:text-[#FFAC1C]"
>
{link.label}
</a>
</li>
))}
</ul>
)}
</div>
)
}
Loading
Loading