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. +