|
| 1 | +"use client"; |
| 2 | + |
| 3 | +import { baseOptions } from "@/app/layout.config"; |
| 4 | +import { Footer } from "@/components/Footer"; |
| 5 | +import { HomeLayout } from "fumadocs-ui/layouts/home"; |
| 6 | +import Link from "next/link"; |
| 7 | +import { usePathname } from "next/navigation"; |
| 8 | +import { useEffect, useState } from "react"; |
| 9 | +import { FaBook, FaCode, FaSearch } from "react-icons/fa"; |
| 10 | + |
| 11 | +interface SearchResult { |
| 12 | + id: string; |
| 13 | + type: "page" | "heading" | "text"; |
| 14 | + content: string; |
| 15 | + url: string; |
| 16 | +} |
| 17 | + |
| 18 | +// Fallback popular pages when search doesn't return results |
| 19 | +const fallbackPages: SearchResult[] = [ |
| 20 | + { |
| 21 | + id: "/docs/getting-started", |
| 22 | + type: "page", |
| 23 | + content: "Getting Started", |
| 24 | + url: "/docs/getting-started", |
| 25 | + }, |
| 26 | + { |
| 27 | + id: "/docs/install", |
| 28 | + type: "page", |
| 29 | + content: "Installation", |
| 30 | + url: "/docs/install", |
| 31 | + }, |
| 32 | + { |
| 33 | + id: "/examples/01-basic/01-minimal", |
| 34 | + type: "page", |
| 35 | + content: "Basic Example", |
| 36 | + url: "/examples/01-basic/01-minimal", |
| 37 | + }, |
| 38 | + { |
| 39 | + id: "/docs/react/components", |
| 40 | + type: "page", |
| 41 | + content: "React Components", |
| 42 | + url: "/docs/react/components", |
| 43 | + }, |
| 44 | + { |
| 45 | + id: "/docs/foundations/document-structure", |
| 46 | + type: "page", |
| 47 | + content: "Document Structure", |
| 48 | + url: "/docs/foundations/document-structure", |
| 49 | + }, |
| 50 | + { |
| 51 | + id: "/docs/features/ai", |
| 52 | + type: "page", |
| 53 | + content: "AI Features", |
| 54 | + url: "/docs/features/ai", |
| 55 | + }, |
| 56 | +]; |
| 57 | + |
| 58 | +export default function NotFound() { |
| 59 | + const pathname = usePathname(); |
| 60 | + const [searchResults, setSearchResults] = useState<SearchResult[]>([]); |
| 61 | + const [isLoading, setIsLoading] = useState(true); |
| 62 | + |
| 63 | + useEffect(() => { |
| 64 | + const searchSimilarPages = async () => { |
| 65 | + try { |
| 66 | + const pathSegments = pathname.split("/").filter(Boolean); |
| 67 | + const searchTerms = pathSegments.slice(-2).join(" "); |
| 68 | + |
| 69 | + if (!searchTerms) { |
| 70 | + throw new Error("No search terms"); |
| 71 | + } |
| 72 | + |
| 73 | + const response = await fetch( |
| 74 | + `/api/search?query=${encodeURIComponent(searchTerms)}`, |
| 75 | + ); |
| 76 | + const data = await response.json(); |
| 77 | + |
| 78 | + const pageResults = data |
| 79 | + .filter((item: SearchResult) => item.type === "page") |
| 80 | + .slice(0, 8); |
| 81 | + |
| 82 | + setSearchResults(pageResults.length > 0 ? pageResults : fallbackPages); |
| 83 | + } catch (error) { |
| 84 | + setSearchResults(fallbackPages); |
| 85 | + } finally { |
| 86 | + setIsLoading(false); |
| 87 | + } |
| 88 | + }; |
| 89 | + |
| 90 | + const timer = setTimeout(searchSimilarPages, 100); |
| 91 | + return () => clearTimeout(timer); |
| 92 | + }, [pathname]); |
| 93 | + |
| 94 | + const useFallback = |
| 95 | + searchResults.length === 0 || searchResults === fallbackPages; |
| 96 | + |
| 97 | + return ( |
| 98 | + <> |
| 99 | + <HomeLayout {...baseOptions}> |
| 100 | + <div className="mx-auto max-w-4xl pt-8"> |
| 101 | + {/* 404 Header */} |
| 102 | + <div className="mb-12 text-center"> |
| 103 | + <div className="mb-6 text-8xl font-bold text-gray-200 dark:text-gray-700"> |
| 104 | + 404 |
| 105 | + </div> |
| 106 | + <h1 className="mb-4 text-4xl font-bold text-gray-900 dark:text-white"> |
| 107 | + Page Not Found |
| 108 | + </h1> |
| 109 | + <p className="mx-auto max-w-2xl text-xl text-gray-600 dark:text-gray-300"> |
| 110 | + We couldn't find the page you're looking for, but here are some |
| 111 | + pages that might help: |
| 112 | + </p> |
| 113 | + </div> |
| 114 | + |
| 115 | + {/* Search Results */} |
| 116 | + <div className="mb-12"> |
| 117 | + {isLoading ? ( |
| 118 | + <div className="flex items-center justify-center space-x-3 py-12 text-gray-600 dark:text-gray-400"> |
| 119 | + <div className="h-6 w-6 animate-spin rounded-full border-b-2 border-blue-600"></div> |
| 120 | + <span className="text-lg">Searching for similar pages...</span> |
| 121 | + </div> |
| 122 | + ) : searchResults.length > 0 ? ( |
| 123 | + <div className="space-y-6"> |
| 124 | + <h2 className="mb-8 text-center text-2xl font-semibold text-gray-900 dark:text-white"> |
| 125 | + {useFallback ? "Popular Pages" : "Similar Pages"} |
| 126 | + </h2> |
| 127 | + <div className="grid grid-cols-1 gap-3 md:grid-cols-2"> |
| 128 | + {searchResults.map((result, index) => ( |
| 129 | + <Link |
| 130 | + key={index} |
| 131 | + href={result.url} |
| 132 | + className="group block rounded-lg border border-gray-200 bg-white p-4 text-left transition-all duration-200 hover:border-blue-300 hover:shadow-md dark:border-gray-700 dark:bg-gray-800 dark:hover:border-blue-600" |
| 133 | + > |
| 134 | + <div className="flex items-center space-x-4"> |
| 135 | + <div className="flex-shrink-0"> |
| 136 | + {result.url.includes("/docs") ? ( |
| 137 | + <FaBook className="h-5 w-5 text-blue-600 group-hover:text-blue-700" /> |
| 138 | + ) : result.url.includes("/examples") ? ( |
| 139 | + <FaCode className="h-5 w-5 text-green-600 group-hover:text-green-700" /> |
| 140 | + ) : ( |
| 141 | + <FaSearch className="h-5 w-5 text-gray-400 group-hover:text-gray-500" /> |
| 142 | + )} |
| 143 | + </div> |
| 144 | + <div className="min-w-0 flex-1"> |
| 145 | + <h3 className="text-lg font-medium text-gray-900 transition-colors group-hover:text-blue-600 dark:text-white dark:group-hover:text-blue-400"> |
| 146 | + {result.content} |
| 147 | + </h3> |
| 148 | + <p className="mt-1 font-mono text-sm text-gray-400 dark:text-gray-500"> |
| 149 | + {result.url} |
| 150 | + </p> |
| 151 | + </div> |
| 152 | + </div> |
| 153 | + </Link> |
| 154 | + ))} |
| 155 | + </div> |
| 156 | + </div> |
| 157 | + ) : ( |
| 158 | + <div className="py-12 text-center text-gray-600 dark:text-gray-400"> |
| 159 | + <p className="text-lg"> |
| 160 | + No similar pages found. Try searching or browsing our |
| 161 | + documentation. |
| 162 | + </p> |
| 163 | + </div> |
| 164 | + )} |
| 165 | + </div> |
| 166 | + </div> |
| 167 | + </HomeLayout> |
| 168 | + <Footer /> |
| 169 | + </> |
| 170 | + ); |
| 171 | +} |
0 commit comments