mirror of
https://github.com/netbirdio/docs.git
synced 2026-08-25 09:11:26 +02:00
Restructure Troubleshooting into a hub with per-area pages (#814)
* Restructure Troubleshooting into a hub with per-area pages - Add a Troubleshooting hub (/help/troubleshooting) with icon/chip cards and a "Still stuck?" CTA - Split NetBird Client troubleshooting into an overview + per-OS pages (Linux, Windows, macOS, Android, iOS) - Split Self-hosted troubleshooting into an overview + per-area pages (installation, IdP, dashboard, certificates, connectivity, database) - Split "Report bugs and issues" into Community Support and NetBird Support pages - Add Troubleshooting resource connectivity and a NetBird Cloud pending-approval page - Add DNS troubleshooting Issue 8 (Windows NRPT rule blocked by a lingering GPO) - Cross-reference the new pages from networks, DNS, and reverse-proxy docs; update nav Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address review: client terminology, dead props, labels, cross-links - Use "client" instead of "agent" across the client troubleshooting pages (headings, prose, anchors) - Remove unused source: props from the Troubleshooting hub tiles - Relabel the "NetBird Cloud" grouping to "Cloud & identity" (SSO/provisioning also apply to self-hosted) - Add a Tiles title on the report-bug landing; add reverse-proxy -> resource-connectivity cross-link - Fix comma splices introduced by the em-dash cleanup in relayed-connections Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add client-side hash redirect for moved self-hosted anchors Old deep links like /selfhosted/troubleshooting#debugging-turn-connections now forward to the per-area page, since next.config redirects can't act on the URL fragment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Apply docs-skill review: conventions + reshape area pages - "open source" (no hyphen), expand NRPT on first use, descriptive alt text + captions on TURN images - Fix inherited "Netbird" casing in the client glossary - Reshape the six self-hosted area pages to Symptom -> likely causes (ordered) -> Fix -> Confirm, preserving anchored headings Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix two typos in client glossary (CodeRabbit) - "nunning" -> "running" in the glossary - possessive "it's" -> "its" in the routing-table sentence Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: fix two broken links in troubleshooting pages - database: point the "upgrade path" link at /selfhosted/maintenance/upgrade; selfhosted-quickstart has no #upgrade anchor so the old link landed at page top - client: add HashRedirect so old #net-bird-agent-status deep links forward to the renamed #net-bird-client-status section on the same page * docs: address review follow-ups (deep-link redirects + client casing) - self-hosted troubleshooting: extend the HashRedirect map with the per-issue (###-level) anchors from the old single page, so old deep links land on the exact sub-section of the new area page rather than just the page top - client glossary: lowercase "NetBird client" in the peer-a/peer-b entries (house convention) and fix "linux" -> "Linux" * docs: review polish — fix image class + first-use acronym glosses - connectivity: fix bad CSS class imagewrapper-nig -> imagewrapper on the TURN-test screenshot (the typo'd class matched no style and broke zoom) - gloss acronyms on first use: GPO (DNS Issue 8), IdP/SSO (identity-provider), ACME (certificates), CORS (dashboard) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Jack Carter <128555021+SunsetDrifter@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
ffb72cfa34
commit
5729ad035e
24
src/components/HashRedirect.jsx
Normal file
24
src/components/HashRedirect.jsx
Normal file
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
@@ -856,14 +877,117 @@ export const docsNavigation = [
|
||||
title: 'GET MORE HELP',
|
||||
links: [
|
||||
{
|
||||
title: 'Troubleshooting client issues',
|
||||
href: '/help/troubleshooting-client',
|
||||
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: 'Troubleshooting relayed connections',
|
||||
href: '/help/troubleshooting-relayed-connections',
|
||||
},
|
||||
{ title: 'Report bugs and issues', href: '/help/report-bug-issues' },
|
||||
{
|
||||
title: 'Support Matrix',
|
||||
isOpen: false,
|
||||
|
||||
49
src/components/StillStuck.jsx
Normal file
49
src/components/StillStuck.jsx
Normal file
@@ -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
src/components/SupportBanner.jsx
Normal file
78
src/components/SupportBanner.jsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
96
src/components/TroubleshootingStart.jsx
Normal file
96
src/components/TroubleshootingStart.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
/**
|
||||
* "Start here" diagnostics block: a highlighted panel with a short intro and a
|
||||
* row of numbered step cards. Each step can link to a section (href) and/or
|
||||
* show the relevant command.
|
||||
*
|
||||
* @param {string} [eyebrow='Start here'] - Small uppercase label
|
||||
* @param {string} title - Panel title (e.g. "Collect diagnostics first")
|
||||
* @param {string} [id] - Optional id for the title anchor
|
||||
* @param {string} [description] - Intro text below the title
|
||||
* @param {Array<{label: string, title: string, command?: string, href?: string, hint?: string}>} steps
|
||||
*/
|
||||
export function TroubleshootingStart({
|
||||
eyebrow = 'Start here',
|
||||
title,
|
||||
id,
|
||||
description,
|
||||
steps = [],
|
||||
}) {
|
||||
return (
|
||||
<div className="not-prose my-12 overflow-hidden rounded-2xl border-l-4 border-[#F28C28] bg-zinc-50 ring-1 ring-inset ring-zinc-900/7.5 dark:bg-white/2.5 dark:ring-white/10">
|
||||
<div className="p-6">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[#F28C28]">
|
||||
{eyebrow}
|
||||
</p>
|
||||
{title && (
|
||||
<h2
|
||||
id={id}
|
||||
className="mt-1 scroll-mt-24 text-xl font-semibold text-zinc-900 dark:text-white"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{description && (
|
||||
<p className="mt-2 max-w-3xl text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{steps.length > 0 && (
|
||||
<div
|
||||
className={`mt-6 grid grid-cols-1 gap-4 ${
|
||||
steps.length === 2 ? 'sm:grid-cols-2' : 'sm:grid-cols-3'
|
||||
}`}
|
||||
>
|
||||
{steps.map((step) => {
|
||||
const content = (
|
||||
<>
|
||||
<p className="text-xs font-medium text-zinc-500 dark:text-zinc-400">
|
||||
{step.label}
|
||||
</p>
|
||||
<p className="mt-1 flex items-center gap-x-1 text-sm font-semibold text-zinc-900 dark:text-white">
|
||||
{step.title}
|
||||
{step.href && (
|
||||
<span aria-hidden="true" className="text-[#F28C28]">
|
||||
→
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
{step.command && (
|
||||
<code className="mt-3 block overflow-x-auto rounded-lg bg-zinc-900/5 px-3 py-2 font-mono text-sm text-zinc-800 ring-1 ring-inset ring-zinc-900/10 dark:bg-black/40 dark:text-zinc-200 dark:ring-white/10">
|
||||
{step.command}
|
||||
</code>
|
||||
)}
|
||||
{step.hint && (
|
||||
<p className="mt-2 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{step.hint}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
const base =
|
||||
'block rounded-xl bg-white p-4 ring-1 ring-inset ring-zinc-900/7.5 dark:bg-white/5 dark:ring-white/10'
|
||||
|
||||
return step.href ? (
|
||||
<Link
|
||||
key={step.title}
|
||||
href={step.href}
|
||||
className={`${base} transition hover:ring-zinc-900/15 hover:shadow-sm dark:hover:ring-white/20`}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
) : (
|
||||
<div key={step.title} className={base}>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
235
src/components/TroubleshootingTiles.jsx
Normal file
235
src/components/TroubleshootingTiles.jsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import Link from 'next/link'
|
||||
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion'
|
||||
|
||||
import { GridPattern } from '@/components/GridPattern'
|
||||
import { Heading } from '@/components/Heading'
|
||||
|
||||
// Monochrome inline SVG icons. They use `currentColor` so they inherit the
|
||||
// card's text color and work in both light and dark mode. These are recreated
|
||||
// here on purpose — we do not reference the bundled asset IDs from the design
|
||||
// mock, which would not resolve in this project.
|
||||
const icons = {
|
||||
laptop: (
|
||||
<>
|
||||
<rect x="4" y="5" width="16" height="11" rx="1.5" />
|
||||
<path d="M2 20h20" />
|
||||
</>
|
||||
),
|
||||
cloud: <path d="M7 18a4 4 0 0 1 0-8 5 5 0 0 1 9.6-1.5A3.5 3.5 0 0 1 17 18H7Z" />,
|
||||
server: (
|
||||
<>
|
||||
<rect x="4" y="4" width="16" height="6" rx="1.5" />
|
||||
<rect x="4" y="14" width="16" height="6" rx="1.5" />
|
||||
<path d="M8 7h.01M8 17h.01" />
|
||||
</>
|
||||
),
|
||||
firewall: (
|
||||
<>
|
||||
<rect x="3" y="5" width="18" height="14" rx="1" />
|
||||
<path d="M3 10h18M3 14h18M12 5v5M8 10v4M16 10v4M12 14v5" />
|
||||
</>
|
||||
),
|
||||
gear: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9 7 7M17 17l2.1 2.1M19.1 4.9 17 7M7 17l-2.1 2.1" />
|
||||
</>
|
||||
),
|
||||
globe: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M3 12h18M12 3c2.5 2.5 2.5 15 0 18M12 3c-2.5 2.5-2.5 15 0 18" />
|
||||
</>
|
||||
),
|
||||
shield: <path d="M12 3l7 3v5c0 4.5-3 8-7 10-4-2-7-5.5-7-10V6l7-3Z" />,
|
||||
terminal: (
|
||||
<>
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" />
|
||||
<path d="M7 10l3 2-3 2M13 15h4" />
|
||||
</>
|
||||
),
|
||||
windows: (
|
||||
<>
|
||||
<rect x="3.5" y="3.5" width="7" height="7" rx="0.5" />
|
||||
<rect x="13.5" y="3.5" width="7" height="7" rx="0.5" />
|
||||
<rect x="3.5" y="13.5" width="7" height="7" rx="0.5" />
|
||||
<rect x="13.5" y="13.5" width="7" height="7" rx="0.5" />
|
||||
</>
|
||||
),
|
||||
android: (
|
||||
<>
|
||||
<path d="M6 12a6 6 0 0 1 12 0v6a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-6Z" />
|
||||
<path d="M8.5 6.5 7.2 4.6M15.5 6.5l1.3-1.9" />
|
||||
<path d="M9.5 11.5h.01M14.5 11.5h.01" />
|
||||
</>
|
||||
),
|
||||
mobile: (
|
||||
<>
|
||||
<rect x="7" y="3" width="10" height="18" rx="2" />
|
||||
<path d="M11 18h2" />
|
||||
</>
|
||||
),
|
||||
lock: (
|
||||
<>
|
||||
<rect x="5" y="11" width="14" height="9" rx="2" />
|
||||
<path d="M8 11V8a4 4 0 0 1 8 0v3" />
|
||||
</>
|
||||
),
|
||||
database: (
|
||||
<>
|
||||
<ellipse cx="12" cy="6" rx="7" ry="3" />
|
||||
<path d="M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6" />
|
||||
<path d="M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3" />
|
||||
</>
|
||||
),
|
||||
}
|
||||
|
||||
function TileIcon({ name }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-6 w-6"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{icons[name] ?? icons.gear}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function TilePattern({ mouseX, mouseY }) {
|
||||
let maskImage = useMotionTemplate`radial-gradient(180px at ${mouseX}px ${mouseY}px, white, transparent)`
|
||||
let style = { maskImage, WebkitMaskImage: maskImage }
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none">
|
||||
<div className="absolute inset-0 rounded-2xl transition duration-300 [mask-image:linear-gradient(white,transparent)] group-hover:opacity-50">
|
||||
<GridPattern
|
||||
width={72}
|
||||
height={56}
|
||||
x="50%"
|
||||
className="absolute inset-x-0 inset-y-[-30%] h-[160%] w-full skew-y-[-18deg] fill-black/[0.02] stroke-black/5 dark:fill-white/1 dark:stroke-white/2.5"
|
||||
/>
|
||||
</div>
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-2xl bg-gradient-to-r from-[#FFAC1C] to-[#F28C28] opacity-0 transition duration-300 group-hover:opacity-30 dark:group-hover:opacity-60 dark:from-[#F28C28]/30 dark:to-[#FF7518]/30"
|
||||
style={style}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TroubleshootingCard({ item }) {
|
||||
let mouseX = useMotionValue(0)
|
||||
let mouseY = useMotionValue(0)
|
||||
|
||||
function onMouseMove({ currentTarget, clientX, clientY }) {
|
||||
let { left, top } = currentTarget.getBoundingClientRect()
|
||||
mouseX.set(clientX - left)
|
||||
mouseY.set(clientY - top)
|
||||
}
|
||||
|
||||
return (
|
||||
// Plain container <div> — NOT an <a>. The title link and each chip are the
|
||||
// only interactive elements, which keeps the markup free of nested
|
||||
// interactive (link-inside-link) HTML. A card without a dedicated landing
|
||||
// page (e.g. NetBird Cloud) simply renders its title as plain text; its
|
||||
// chips still link to the relevant docs.
|
||||
<div
|
||||
onMouseMove={onMouseMove}
|
||||
className="group relative flex flex-col rounded-2xl bg-zinc-50 transition-shadow hover:shadow-md hover:shadow-zinc-900/5 dark:bg-white/2.5 dark:hover:shadow-black/5"
|
||||
>
|
||||
<TilePattern mouseX={mouseX} mouseY={mouseY} />
|
||||
<div className="absolute inset-0 rounded-2xl ring-1 ring-inset ring-zinc-900/7.5 group-hover:ring-zinc-900/10 dark:ring-white/10 dark:group-hover:ring-white/20" />
|
||||
<div className="relative flex flex-1 flex-col p-4">
|
||||
<div className="flex items-center gap-x-3">
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-white text-zinc-700 ring-1 ring-inset ring-zinc-900/7.5 dark:bg-white/5 dark:text-zinc-300 dark:ring-white/10">
|
||||
<TileIcon name={item.icon} />
|
||||
</span>
|
||||
<h3 className="text-sm font-semibold leading-7 text-zinc-900 dark:text-white">
|
||||
{item.href ? (
|
||||
<Link
|
||||
href={item.href}
|
||||
className="transition hover:text-[#F28C28] dark:hover:text-[#FFAC1C]"
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
) : (
|
||||
item.title
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{item.description && (
|
||||
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{item.chips && item.chips.length > 0 && (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{item.chips.map((chip) => (
|
||||
<Link
|
||||
key={chip.href}
|
||||
href={chip.href}
|
||||
className="inline-flex items-center rounded-full bg-zinc-900/5 px-2.5 py-1 text-xs font-medium text-zinc-700 ring-1 ring-inset ring-zinc-900/10 transition hover:bg-zinc-900/10 hover:text-zinc-900 dark:bg-white/5 dark:text-zinc-300 dark:ring-white/10 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
{chip.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Troubleshooting hub grid: richer icon + chip cards.
|
||||
*
|
||||
* @param {string} [title] - Section heading (e.g. "Find your issue by area")
|
||||
* @param {string} [id] - Optional id for the heading anchor
|
||||
* @param {string} [description] - Optional description below the title
|
||||
* @param {Array<{
|
||||
* title: string,
|
||||
* href?: string, // title links here; omit for a "coming soon" card
|
||||
* icon: string, // key into the inline icon set
|
||||
* description?: string,
|
||||
* chips?: Array<{label: string, href: string}>
|
||||
* }>} items
|
||||
*/
|
||||
export function TroubleshootingTiles({ title, id, description, items }) {
|
||||
const hasHeader = title || description
|
||||
|
||||
return (
|
||||
<div className="my-16 xl:max-w-none">
|
||||
{title && (
|
||||
<Heading level={2} id={id} anchor={!!id}>
|
||||
{title}
|
||||
</Heading>
|
||||
)}
|
||||
{description && (
|
||||
<div
|
||||
className={`text-sm text-zinc-600 dark:text-zinc-400 ${title ? 'mt-4' : ''}`}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`not-prose grid grid-cols-1 gap-8 sm:grid-cols-2 ${
|
||||
hasHeader
|
||||
? 'mt-4 border-t border-zinc-900/5 pt-10 dark:border-white/5'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<TroubleshootingCard key={item.title} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user