From a29dc5de4ba8ee54ec05896a37b8f2ac9ac22e0b Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:10:12 -0400 Subject: [PATCH] feat(docs): main troubleshooting page (#29120) * feat(docs): main troubleshooting page * feat(docs): global troubleshooting page layout * feat(docs): global troubleshooting page filter logic * enhance(docs): troubleshooting page ui & filters Improve UX and accessibility of global troubleshooting search page. - Implement TroubleshootingFilterStateProvider for managing filter state - Add TroubleshootingFilterEmptyState component - Improve TroubleshootingFilter UI with applied filters display - Update TroubleshootingPreview component with better layout and accessibility - Add TroubleshootingEntryAssociatedErrors component for displaying related errors - Refactor Troubleshooting utils and schemas - Update troubleshooting content template * feat(docs): individual troubleshooting pages Add individual pages for detailed information on troubleshooting entries. Small fixes to UI for troubleshooting displays. - Wrap GlobalTroubleshootingPage with SidebarSkeleton - Add TroubleshootingErrorListDetailed component for detailed error display - Implement TroubleshootingBackLink component for navigation - Update TroubleshootingPreview to use Next.js Link and include parent page - Refactor Footer component to accept className prop - Add hideFooter option to SidebarSkeleton - Minor updates to TopNavBar and other utility functions * fix: minor styling issues * tweak(troubleshooting docs): use breadcrumbs * tweak(troubleshooting docs): use sidebar for filter * enhance(troubleshooting docs): navigation, keywords Better navigation for individual troubleshooting entries, using a version of the sidebar + keywords handling via search params for good linking behavior Details: - Add TroubleshootingSidebar to TroubleshootingPage - Implement query state management for keywords using nuqs - Improve accessibility and styling of troubleshooting components - Update empty state handling and related keywords display - Upgrade nuqs package to version 1.19.1 * fix(docs): wrap useSearchParams in Suspense * seo(docs): add canonical link to troubleshooting * enhance(docs,troubleshooting): auto-reveal errors On troubleshooting search pages, if the searched term matches a hidden error, auto-reveal it when searching. When the search term is cleared, hide programmatically-opened errors while preserving user-opened ones. * enhance(docs,troubleshooting): search bar Add search bar to individual troubleshooting pages * fix(docs): breadcrumb links Fix breadcrumb linking so it uses next/link instead of reloading entire app on navigation. * fix(docs): typo * enh(docs): new troubleshooting design * enh(docs): use new multi-select * enh(docs): troubleshooting responsive styles * Update apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx --- .../{ => (with-sidebar)}/[...slug]/page.tsx | 0 .../ai/python/[slug]/page.tsx | 0 .../{ => (with-sidebar)}/cli/config/page.tsx | 0 .../cli/github-action/[slug]/page.tsx | 0 .../database/database-advisors/page.tsx | 0 .../extensions/wrappers/[[...slug]]/page.tsx | 0 .../graphql/[[...slug]]/page.tsx | 0 .../guides/{ => (with-sidebar)}/layout.tsx | 0 .../platform/terraform/[[...slug]]/page.tsx | 0 .../platform/terraform/reference/page.tsx | 0 .../platform/terraform/terraformConstants.ts | 0 .../self-hosting/analytics/config/page.tsx | 0 .../self-hosting/auth/config/page.tsx | 0 .../self-hosting/realtime/config/page.tsx | 0 .../self-hosting/storage/config/page.tsx | 0 .../guides/troubleshooting/[slug]/page.tsx | 37 ++ apps/docs/app/guides/troubleshooting/page.tsx | 61 +++ apps/docs/components/Breadcrumbs.tsx | 65 ++- apps/docs/components/Feedback/Feedback.tsx | 6 +- apps/docs/components/MultiSelect.client.tsx | 196 ++++++++ apps/docs/components/Navigation/Footer.tsx | 6 +- .../NavigationMenu.constants.ts | 5 + .../content/troubleshooting/_template.mdx | 4 +- ...monitor-supavisor-postgres-connections.mdx | 112 +++++ .../supabase-grafana-memory-charts.mdx | 60 +++ .../features/docs/Troubleshooting.page.tsx | 103 ++++ .../docs/Troubleshooting.ui.client.tsx | 448 ++++++++++++++++++ .../docs/features/docs/Troubleshooting.ui.tsx | 75 +++ .../docs/Troubleshooting.utils.shared.ts | 24 + .../features/docs/Troubleshooting.utils.ts | 221 +++++++-- apps/docs/layouts/DefaultLayout.tsx | 3 +- apps/docs/layouts/MainSkeleton.tsx | 35 +- apps/docs/lib/constants.ts | 1 + apps/docs/lib/docs.ts | 2 +- apps/docs/next-env.d.ts | 3 +- apps/docs/package.json | 1 + apps/docs/styles/main.scss | 7 + package-lock.json | 8 +- 38 files changed, 1405 insertions(+), 78 deletions(-) rename apps/docs/app/guides/{ => (with-sidebar)}/[...slug]/page.tsx (100%) rename apps/docs/app/guides/{ => (with-sidebar)}/ai/python/[slug]/page.tsx (100%) rename apps/docs/app/guides/{ => (with-sidebar)}/cli/config/page.tsx (100%) rename apps/docs/app/guides/{ => (with-sidebar)}/cli/github-action/[slug]/page.tsx (100%) rename apps/docs/app/guides/{ => (with-sidebar)}/database/database-advisors/page.tsx (100%) rename apps/docs/app/guides/{ => (with-sidebar)}/database/extensions/wrappers/[[...slug]]/page.tsx (100%) rename apps/docs/app/guides/{ => (with-sidebar)}/graphql/[[...slug]]/page.tsx (100%) rename apps/docs/app/guides/{ => (with-sidebar)}/layout.tsx (100%) rename apps/docs/app/guides/{ => (with-sidebar)}/platform/terraform/[[...slug]]/page.tsx (100%) rename apps/docs/app/guides/{ => (with-sidebar)}/platform/terraform/reference/page.tsx (100%) rename apps/docs/app/guides/{ => (with-sidebar)}/platform/terraform/terraformConstants.ts (100%) rename apps/docs/app/guides/{ => (with-sidebar)}/self-hosting/analytics/config/page.tsx (100%) rename apps/docs/app/guides/{ => (with-sidebar)}/self-hosting/auth/config/page.tsx (100%) rename apps/docs/app/guides/{ => (with-sidebar)}/self-hosting/realtime/config/page.tsx (100%) rename apps/docs/app/guides/{ => (with-sidebar)}/self-hosting/storage/config/page.tsx (100%) create mode 100644 apps/docs/app/guides/troubleshooting/[slug]/page.tsx create mode 100644 apps/docs/app/guides/troubleshooting/page.tsx create mode 100644 apps/docs/components/MultiSelect.client.tsx create mode 100644 apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx create mode 100644 apps/docs/content/troubleshooting/supabase-grafana-memory-charts.mdx create mode 100644 apps/docs/features/docs/Troubleshooting.page.tsx create mode 100644 apps/docs/features/docs/Troubleshooting.ui.client.tsx create mode 100644 apps/docs/features/docs/Troubleshooting.ui.tsx create mode 100644 apps/docs/features/docs/Troubleshooting.utils.shared.ts diff --git a/apps/docs/app/guides/[...slug]/page.tsx b/apps/docs/app/guides/(with-sidebar)/[...slug]/page.tsx similarity index 100% rename from apps/docs/app/guides/[...slug]/page.tsx rename to apps/docs/app/guides/(with-sidebar)/[...slug]/page.tsx diff --git a/apps/docs/app/guides/ai/python/[slug]/page.tsx b/apps/docs/app/guides/(with-sidebar)/ai/python/[slug]/page.tsx similarity index 100% rename from apps/docs/app/guides/ai/python/[slug]/page.tsx rename to apps/docs/app/guides/(with-sidebar)/ai/python/[slug]/page.tsx diff --git a/apps/docs/app/guides/cli/config/page.tsx b/apps/docs/app/guides/(with-sidebar)/cli/config/page.tsx similarity index 100% rename from apps/docs/app/guides/cli/config/page.tsx rename to apps/docs/app/guides/(with-sidebar)/cli/config/page.tsx diff --git a/apps/docs/app/guides/cli/github-action/[slug]/page.tsx b/apps/docs/app/guides/(with-sidebar)/cli/github-action/[slug]/page.tsx similarity index 100% rename from apps/docs/app/guides/cli/github-action/[slug]/page.tsx rename to apps/docs/app/guides/(with-sidebar)/cli/github-action/[slug]/page.tsx diff --git a/apps/docs/app/guides/database/database-advisors/page.tsx b/apps/docs/app/guides/(with-sidebar)/database/database-advisors/page.tsx similarity index 100% rename from apps/docs/app/guides/database/database-advisors/page.tsx rename to apps/docs/app/guides/(with-sidebar)/database/database-advisors/page.tsx diff --git a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx b/apps/docs/app/guides/(with-sidebar)/database/extensions/wrappers/[[...slug]]/page.tsx similarity index 100% rename from apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx rename to apps/docs/app/guides/(with-sidebar)/database/extensions/wrappers/[[...slug]]/page.tsx diff --git a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx b/apps/docs/app/guides/(with-sidebar)/graphql/[[...slug]]/page.tsx similarity index 100% rename from apps/docs/app/guides/graphql/[[...slug]]/page.tsx rename to apps/docs/app/guides/(with-sidebar)/graphql/[[...slug]]/page.tsx diff --git a/apps/docs/app/guides/layout.tsx b/apps/docs/app/guides/(with-sidebar)/layout.tsx similarity index 100% rename from apps/docs/app/guides/layout.tsx rename to apps/docs/app/guides/(with-sidebar)/layout.tsx diff --git a/apps/docs/app/guides/platform/terraform/[[...slug]]/page.tsx b/apps/docs/app/guides/(with-sidebar)/platform/terraform/[[...slug]]/page.tsx similarity index 100% rename from apps/docs/app/guides/platform/terraform/[[...slug]]/page.tsx rename to apps/docs/app/guides/(with-sidebar)/platform/terraform/[[...slug]]/page.tsx diff --git a/apps/docs/app/guides/platform/terraform/reference/page.tsx b/apps/docs/app/guides/(with-sidebar)/platform/terraform/reference/page.tsx similarity index 100% rename from apps/docs/app/guides/platform/terraform/reference/page.tsx rename to apps/docs/app/guides/(with-sidebar)/platform/terraform/reference/page.tsx diff --git a/apps/docs/app/guides/platform/terraform/terraformConstants.ts b/apps/docs/app/guides/(with-sidebar)/platform/terraform/terraformConstants.ts similarity index 100% rename from apps/docs/app/guides/platform/terraform/terraformConstants.ts rename to apps/docs/app/guides/(with-sidebar)/platform/terraform/terraformConstants.ts diff --git a/apps/docs/app/guides/self-hosting/analytics/config/page.tsx b/apps/docs/app/guides/(with-sidebar)/self-hosting/analytics/config/page.tsx similarity index 100% rename from apps/docs/app/guides/self-hosting/analytics/config/page.tsx rename to apps/docs/app/guides/(with-sidebar)/self-hosting/analytics/config/page.tsx diff --git a/apps/docs/app/guides/self-hosting/auth/config/page.tsx b/apps/docs/app/guides/(with-sidebar)/self-hosting/auth/config/page.tsx similarity index 100% rename from apps/docs/app/guides/self-hosting/auth/config/page.tsx rename to apps/docs/app/guides/(with-sidebar)/self-hosting/auth/config/page.tsx diff --git a/apps/docs/app/guides/self-hosting/realtime/config/page.tsx b/apps/docs/app/guides/(with-sidebar)/self-hosting/realtime/config/page.tsx similarity index 100% rename from apps/docs/app/guides/self-hosting/realtime/config/page.tsx rename to apps/docs/app/guides/(with-sidebar)/self-hosting/realtime/config/page.tsx diff --git a/apps/docs/app/guides/self-hosting/storage/config/page.tsx b/apps/docs/app/guides/(with-sidebar)/self-hosting/storage/config/page.tsx similarity index 100% rename from apps/docs/app/guides/self-hosting/storage/config/page.tsx rename to apps/docs/app/guides/(with-sidebar)/self-hosting/storage/config/page.tsx diff --git a/apps/docs/app/guides/troubleshooting/[slug]/page.tsx b/apps/docs/app/guides/troubleshooting/[slug]/page.tsx new file mode 100644 index 00000000000..428a2e84adc --- /dev/null +++ b/apps/docs/app/guides/troubleshooting/[slug]/page.tsx @@ -0,0 +1,37 @@ +import { notFound } from 'next/navigation' + +import TroubleshootingPage from '~/features/docs/Troubleshooting.page' +import { getAllTroubleshootingEntries, getArticleSlug } from '~/features/docs/Troubleshooting.utils' +import { PROD_URL } from '~/lib/constants' + +export default async function TroubleshootingEntryPage({ + params: { slug }, +}: { + params: { slug: string } +}) { + const allTroubleshootingEntries = await getAllTroubleshootingEntries() + const entry = allTroubleshootingEntries.find((entry) => getArticleSlug(entry.data) === slug) + + if (!entry) { + notFound() + } + + return +} + +export const generateMetadata = async ({ params: { slug } }: { params: { slug: string } }) => { + const allTroubleshootingEntries = await getAllTroubleshootingEntries() + const entry = allTroubleshootingEntries.find((entry) => getArticleSlug(entry.data) === slug) + + return { + title: 'Supabase Docs | Troubleshooting' + (entry ? ` | ${entry.data.title}` : ''), + alternates: { + canonical: `${PROD_URL}/guides/troubleshooting/${slug}`, + }, + } +} + +export const generateStaticParams = async () => { + const allTroubleshootingEntries = await getAllTroubleshootingEntries() + return allTroubleshootingEntries.map((entry) => ({ slug: getArticleSlug(entry.data) })) +} diff --git a/apps/docs/app/guides/troubleshooting/page.tsx b/apps/docs/app/guides/troubleshooting/page.tsx new file mode 100644 index 00000000000..e5e96fe66df --- /dev/null +++ b/apps/docs/app/guides/troubleshooting/page.tsx @@ -0,0 +1,61 @@ +import { type Metadata } from 'next' + +import { TroubleshootingPreview } from '~/features/docs/Troubleshooting.ui' +import { + TroubleshootingFilter, + TroubleshootingFilterEmptyState, + TroubleshootingListController, +} from '~/features/docs/Troubleshooting.ui.client' +import { + getAllTroubleshootingEntries, + getAllTroubleshootingErrors, + getAllTroubleshootingKeywords, + getAllTroubleshootingProducts, +} from '~/features/docs/Troubleshooting.utils' +import { TROUBLESHOOTING_CONTAINER_ID } from '~/features/docs/Troubleshooting.utils.shared' +import { SidebarSkeleton } from '~/layouts/MainSkeleton' +import { PROD_URL } from '~/lib/constants' + +export default async function GlobalTroubleshootingPage() { + const troubleshootingEntries = await getAllTroubleshootingEntries() + const keywords = await getAllTroubleshootingKeywords() + const products = await getAllTroubleshootingProducts() + const errors = await getAllTroubleshootingErrors() + + return ( + + + Troubleshooting + + Search or browse our troubleshooting guides for solutions to common Supabase issues. + + + + + + + Matching troubleshooting entries + + {troubleshootingEntries.map((entry) => ( + + + + ))} + + + + + ) +} + +export const metadata: Metadata = { + title: 'Supabase Docs | Troubleshooting', + alternates: { + canonical: `${PROD_URL}/guides/troubleshooting`, + }, +} diff --git a/apps/docs/components/Breadcrumbs.tsx b/apps/docs/components/Breadcrumbs.tsx index cc7e96f3d90..398a5112c2a 100644 --- a/apps/docs/components/Breadcrumbs.tsx +++ b/apps/docs/components/Breadcrumbs.tsx @@ -1,8 +1,8 @@ 'use client' import Link from 'next/link' -import { usePathname } from 'next/navigation' -import React, { Fragment } from 'react' +import { usePathname, useSearchParams } from 'next/navigation' +import React, { Fragment, Suspense } from 'react' import { useBreakpoint } from 'common' import { @@ -29,19 +29,36 @@ import { import * as NavItems from '~/components/Navigation/NavigationMenu/NavigationMenu.constants' import { getMenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu.utils' -const Breadcrumbs = ({ className }: { className?: string }) => { - const pathname = usePathname() - const menuId = getMenuId(pathname) - const menu = NavItems[menuId] - const breadcrumbs = findMenuItemByUrl(menu, pathname, []) +interface BreadcrumbsProps extends React.HTMLAttributes { + minLength?: number + forceDisplayOnMobile?: boolean +} + +export default function Breadcrumbs(props: BreadcrumbsProps) { + return ( + + + + ) +} + +const BreadcrumbsInternal = ({ + className, + minLength = 2, + forceDisplayOnMobile = false, +}: BreadcrumbsProps) => { + const breadcrumbs = useBreadcrumbs() const [open, setOpen] = React.useState(false) const isMobile = useBreakpoint('md') const ITEMS_TO_DISPLAY = isMobile ? 4 : 3 - if (!breadcrumbs?.length || breadcrumbs?.length === 1) return null + if (!breadcrumbs?.length || breadcrumbs?.length < minLength) return null - const appendedBreadcrumbs = breadcrumbs?.slice(-ITEMS_TO_DISPLAY + 1, isMobile ? -1 : undefined) + const appendedBreadcrumbs = breadcrumbs?.slice( + -ITEMS_TO_DISPLAY + 1, + isMobile && !forceDisplayOnMobile ? -1 : undefined + ) return ( @@ -50,8 +67,10 @@ const Breadcrumbs = ({ className }: { className?: string }) => { <> {breadcrumbs[0].url ? ( - - {breadcrumbs[0].title || breadcrumbs[0].name} + + + {breadcrumbs[0].title || breadcrumbs[0].name} + ) : ( {breadcrumbs[0].title || breadcrumbs[0].name} @@ -75,7 +94,7 @@ const Breadcrumbs = ({ className }: { className?: string }) => { className={cn(!crumb.url && 'pointer-events-none')} > {crumb.url ? ( - {crumb.title || crumb.name} + {crumb.title || crumb.name} ) : ( crumb.title || crumb.name )} @@ -94,7 +113,7 @@ const Breadcrumbs = ({ className }: { className?: string }) => { .slice(1, -2) .map((crumb) => crumb.url ? ( - {crumb.title || crumb.name} + {crumb.title || crumb.name} ) : ( crumb.title || crumb.name ) @@ -121,8 +140,8 @@ const Breadcrumbs = ({ className }: { className?: string }) => { )} > {crumb.url ? ( - - {crumb.title || crumb.name} + + {crumb.title || crumb.name} ) : ( {crumb.title || crumb.name} @@ -138,6 +157,20 @@ const Breadcrumbs = ({ className }: { className?: string }) => { ) } +function useBreadcrumbs() { + const pathname = usePathname() + + const isTroubleshootingPage = pathname.startsWith('/guides/troubleshooting') + if (isTroubleshootingPage) { + const breadcrumbs = [{ name: 'Troubleshooting', url: '/guides/troubleshooting' }] + return breadcrumbs + } + + const menuId = getMenuId(pathname) + const menu = NavItems[menuId] + return findMenuItemByUrl(menu, pathname, []) +} + function findMenuItemByUrl(menu, targetUrl, parents = []) { // If the menu has items, recursively search through them if (menu.items) { @@ -157,5 +190,3 @@ function findMenuItemByUrl(menu, targetUrl, parents = []) { // If the URL is not found, return null return null } - -export default Breadcrumbs diff --git a/apps/docs/components/Feedback/Feedback.tsx b/apps/docs/components/Feedback/Feedback.tsx index cd0a6341f8b..a5870ad675d 100644 --- a/apps/docs/components/Feedback/Feedback.tsx +++ b/apps/docs/components/Feedback/Feedback.tsx @@ -1,3 +1,5 @@ +'use client' + import { useSupabaseClient } from '@supabase/auth-helpers-react' import { Check, MessageSquareQuote, X } from 'lucide-react' import { usePathname } from 'next/navigation' @@ -69,7 +71,7 @@ function reducer(state: State, action: Action) { } } -function Feedback() { +function Feedback({ className }: { className?: string }) { const [state, dispatch] = useReducer(reducer, initialState) const [modalOpen, setModalOpen] = useState(false) const feedbackButtonRef = useRef(null) @@ -132,7 +134,7 @@ function Feedback() { } return ( - + Is this helpful? diff --git a/apps/docs/components/MultiSelect.client.tsx b/apps/docs/components/MultiSelect.client.tsx new file mode 100644 index 00000000000..a56994cab30 --- /dev/null +++ b/apps/docs/components/MultiSelect.client.tsx @@ -0,0 +1,196 @@ +'use client' + +import { ChevronsUpDown } from 'lucide-react' +import { + createContext, + forwardRef, + useCallback, + useContext, + useId, + useState, + PropsWithChildren, + useRef, + useEffect, +} from 'react' + +import { + cn, + Badge, + Checkbox_Shadcn_ as Checkbox, + Popover_Shadcn_ as Popover, + PopoverContent_Shadcn_ as PopoverContent, + PopoverTrigger_Shadcn_ as PopoverTrigger, +} from 'ui' + +interface Item { + value: string + label: string +} + +const MultiSelectContext = createContext<{ + selected: Item[] + handleSelect: (item: Item) => void + setOpen: (open: boolean) => void +}>({ + selected: [], + handleSelect: () => {}, + setOpen: () => {}, +}) + +function MultiSelectProvider({ + selected, + onSelectedChange, + setOpen, + children, +}: PropsWithChildren<{ + selected: Item[] + onSelectedChange: (selected: Item[] | ((selected: Item[]) => Item[])) => void + setOpen: (open: boolean) => void +}>) { + const handleSelect = useCallback( + (item: Item) => { + onSelectedChange((current: Item[]) => { + const isSelected = current.some((currItem) => currItem.value === item.value) + if (isSelected) { + return current.filter((currItem) => currItem.value !== item.value) + } else { + return [...current, item] + } + }) + }, + [onSelectedChange] + ) + + return ( + + {children} + + ) +} + +function useMultiSelect() { + const context = useContext(MultiSelectContext) + if (!context) { + throw new Error('useMultiSelect must be used within a MultiSelectProvider') + } + return context +} + +export function MultiSelect({ + open: _controlledOpen, + setOpen: _setControlledOpen, + selected, + onSelectedChange, + children, + ...props +}: PropsWithChildren< + { + open?: boolean + setOpen?: React.Dispatch> + selected?: Item[] + onSelectedChange: (selected: Item[] | ((selected: Item[]) => Item[])) => void + } & React.ComponentProps +>) { + const [_internalOpen, _setInternalOpen] = useState(false) + const open = _controlledOpen ?? _internalOpen + const setOpen = _setControlledOpen ?? _setInternalOpen + + return ( + + + {children} + + + ) +} + +const Trigger = forwardRef< + HTMLButtonElement, + { label?: string; className?: string } & React.ComponentProps +>(({ label, className, ...props }, ref) => { + const { selected } = useMultiSelect() + + const [measuredWidth, setMeasuredWidth] = useState(0) + const measuredRef = (node: HTMLElement) => { + if (node) { + setMeasuredWidth(node.getBoundingClientRect().width) + } + } + + const badgeWidth = 100 // Approximate width of a badge in pixels + const maxBadges = Math.max(1, Math.floor((measuredWidth || 200) / badgeWidth)) + const visibleBadges = selected.slice(0, maxBadges) + const extraBadgesCount = Math.max(0, selected.length - maxBadges) + + const badgeClasses = 'bg-200' + + return ( + + + {selected.length === 0 ? ( + // Leading prevents shift when switching to badges + {label} + ) : ( + + {visibleBadges.map((item) => ( + + {item.label} + + ))} + {extraBadgesCount > 0 && +{extraBadgesCount}} + + )} + + + + ) +}) +Trigger.displayName = 'Trigger' +MultiSelect.Trigger = Trigger + +const Content = forwardRef< + HTMLDivElement, + PropsWithChildren<{ className?: string } & React.ComponentProps> +>(({ children, className, ...props }, ref) => { + return ( + + {children} + + ) +}) +Content.displayName = 'Content' +MultiSelect.Content = Content + +export function Item({ item, className }: { item: Item; className?: string }) { + const id = useId() + const { selected, handleSelect } = useMultiSelect() + + return ( + + sel.value === item.value)} + onCheckedChange={() => handleSelect(item)} + /> + + {item.label} + + + ) +} +MultiSelect.Item = Item diff --git a/apps/docs/components/Navigation/Footer.tsx b/apps/docs/components/Navigation/Footer.tsx index 460b228f9de..dff863377c1 100644 --- a/apps/docs/components/Navigation/Footer.tsx +++ b/apps/docs/components/Navigation/Footer.tsx @@ -1,10 +1,10 @@ import Link from 'next/link' -import { Button } from 'ui' +import { Button, cn } from 'ui' import { primaryLinks, secondaryLinks } from '~/data/footer' import { LayoutMainContent } from '~/layouts/DefaultLayout' -const Footer = () => ( - +const Footer = ({ className }: { className?: string }) => ( +
+ Search or browse our troubleshooting guides for solutions to common Supabase issues. +