diff --git a/apps/docs/app/not-found/page.tsx b/apps/docs/app/not-found/page.tsx index 415285cd676..c33e7387be0 100644 --- a/apps/docs/app/not-found/page.tsx +++ b/apps/docs/app/not-found/page.tsx @@ -1,11 +1,11 @@ import { createClient } from '@supabase/supabase-js' import { type Metadata } from 'next' +import Link from 'next/link' import { DocsSearchResult, type Database } from 'common' +import { Button } from 'ui' -import { NotFound } from '~/features/recommendations/NotFound.client' - -export const fetchCache = 'force-no-store' +import { SearchButton, Recommendations } from '~/features/recommendations/NotFound.client' const metadata: Metadata = { robots: { @@ -15,25 +15,34 @@ const metadata: Metadata = { } const NotFoundPage = async ({ searchParams: { page } }: { searchParams: { page?: string } }) => { - const recommendations = page ? await getRecommendations(page) : null - - return -} - -const getRecommendations = async (page: string) => { - const query = decodeURIComponent(page.replace(/^\/(?:guides|reference)\//, '')).replace( - /[_\/-]/g, - ' ' + return ( +
+

404: We couldn't find that page

+

+ Sorry, we couldn't find that page. It might be missing, or we had a temporary error + generating it. +

+
+ + + +
+ +
) - if (!query) return null - - const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! - ) - const { data, error } = await supabase.rpc('docs_search_fts', { query }) - if (error || !data?.length) return null - return data as Omit[] } export default NotFoundPage diff --git a/apps/docs/features/recommendations/NotFound.client.tsx b/apps/docs/features/recommendations/NotFound.client.tsx index e6cbd149aa8..616022bd81c 100644 --- a/apps/docs/features/recommendations/NotFound.client.tsx +++ b/apps/docs/features/recommendations/NotFound.client.tsx @@ -1,69 +1,101 @@ 'use client' +import { useSupabaseClient } from '@supabase/auth-helpers-react' +import { type SupabaseClient } from '@supabase/supabase-js' +import { X } from 'lucide-react' +import { useEffect, useRef, useState } from 'react' + +import { useCommandMenu } from '@ui-patterns/Cmdk' +import { GenericSkeletonLoader } from '@ui-patterns/ShimmeringLoader' import { type DocsSearchResult } from 'common' -import Link from 'next/link' -import { Button } from 'ui' -import { useCommandMenu } from 'ui-patterns/Cmdk' +import { Button, cn } from 'ui' + import ButtonCard from '~/components/ButtonCard' -const NotFound = ({ - recommendations, - omitSearch = false, -}: { - recommendations?: Omit[] | null - omitSearch?: boolean -}) => { +function SearchButton() { const { setIsOpen: setCommandMenuOpen } = useCommandMenu() return ( -
-

We couldn't find that page

-

- Sorry, we couldn't find that page. It might be missing, or we had a temporary error - generating it. -

-
- {!omitSearch && ( - - )} - - -
- {recommendations && ( -
-

Are you looking for...?

-
    - {recommendations - .filter(({ title }) => !!title) - .slice(0, 6) - .map(({ path, title, subtitle, description }) => ( - - ))} -
-
- )} -
+ ) } -export { NotFound } +async function getRecommendations(page: string, supabase: SupabaseClient) { + try { + const query = decodeURIComponent(page.replace(/^\/(?:guides|reference)\//, '')).replace( + /[_\/-]/g, + ' ' + ) + if (!query) return [] + + const { data, error } = await supabase.rpc('docs_search_fts', { query }) + if (error || !data?.length) return [] + return data as Array> + } catch (err) { + console.error(err) + return [] + } +} + +function Recommendations({ page }: { page: string }) { + const supabase = useSupabaseClient() + const [recommendations, setRecommendations] = useState( + [] as Array> + ) + + const fetched = useRef(false) + + useEffect(() => { + getRecommendations(page, supabase).then((data) => { + if (!fetched.current) { + setRecommendations(data) + fetched.current = true + } + }) + }, [page, supabase]) + + return ( +
+

Are you looking for...?

+ {!fetched.current && } + {fetched.current && recommendations.length === 0 && } + {fetched.current && recommendations.length > 0 && ( + + )} +
+ ) +} + +function LoadingState() { + return +} + +function NoResults() { + return No recommendations found. +} + +function RecommendationsList({ + recommendations, +}: { + recommendations: Array> +}) { + return ( +
    + {recommendations + .filter(({ title }) => !!title) + .slice(0, 6) + .map(({ path, title, subtitle, description }) => ( + + ))} +
+ ) +} + +export { SearchButton, Recommendations } diff --git a/apps/docs/styles/main.scss b/apps/docs/styles/main.scss index edd67d0641a..4567d1530f1 100644 --- a/apps/docs/styles/main.scss +++ b/apps/docs/styles/main.scss @@ -302,3 +302,37 @@ th code { [data-rmiz-modal-img] { image-rendering: high-quality; } + +/* Loaders */ + +.shimmering-loader { + animation: shimmer 2s infinite linear; + background: linear-gradient( + to right, + hsl(var(--border-default)) 4%, + hsl(var(--background-surface-200)) 25%, + hsl(var(--border-default)) 36% + ); + background-size: 1000px 100%; +} + +.dark .shimmering-loader { + animation: shimmer 2s infinite linear; + background: linear-gradient( + to right, + hsl(var(--border-default)) 4%, + hsl(var(--border-control)) 25%, + hsl(var(--border-default)) 36% + ); + background-size: 1000px 100%; +} + +@keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } +} +