diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 00000000..e6a0bb5d --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,17 @@ +// Required by Next.js App Router. Every app/ directory MUST have a root layout +// and it MUST include and
— Next.js 14 will not inject them +// automatically, and the App Router runtime crashes on root path without them +// (causing a blank screen on localhost:3000/). +// +// Global CSS is imported here so App Router pages (e.g. not-found.tsx) get +// Tailwind and site styles. Pages Router routes continue to load CSS via +// pages/_app.tsx as before — importing the same file twice is fine (deduped). +import "../styles/index.css"; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/app/not-found-client.tsx b/app/not-found-client.tsx new file mode 100644 index 00000000..5d09ee60 --- /dev/null +++ b/app/not-found-client.tsx @@ -0,0 +1,143 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import PostGrid from "../components/post-grid"; +import PostCard from "../components/post-card"; +import { Post } from "../types/post"; +import { getExcerpt } from "../utils/excerpt"; +import { FaSearch } from "react-icons/fa"; + +interface Props { + latestPosts?: { edges: Array<{ node: Post }> }; + communityPosts?: { edges: Array<{ node: Post }> }; + technologyPosts?: { edges: Array<{ node: Post }> }; +} + +export default function NotFoundClient({ latestPosts, communityPosts, technologyPosts }: Props) { + const [countdown, setCountdown] = useState(12); + const [searchTerm, setSearchTerm] = useState(""); + + useEffect(() => { + const interval = setInterval(() => { + setCountdown((prev) => { + if (prev <= 1) { clearInterval(interval); window.location.href = "/blog"; return 0; } + return prev - 1; + }); + }, 1000); + return () => clearInterval(interval); + }, []); + + const formatTime = (s: number) => s >= 60 ? `${Math.floor(s / 60)}m ${s % 60}s` : `${s}s`; + + const allPosts = useMemo(() => + [...(latestPosts?.edges || []), ...(communityPosts?.edges || []), ...(technologyPosts?.edges || [])] + .filter((p, i, self) => i === self.findIndex((x) => x.node.slug === p.node.slug)), + [latestPosts, communityPosts, technologyPosts] + ); + + const filteredPosts = useMemo(() => { + const t = searchTerm.toLowerCase(); + return allPosts.filter(({ node }) => + (node.title || "").toLowerCase().includes(t) || (node.excerpt || "").toLowerCase().includes(t) + ); + }, [allPosts, searchTerm]); + + return ( ++ Looks like you have wandered off the beaten path. Our team is working to get you back on track and find what you are looking for. +
++ Wait for {formatTime(countdown)} for automatic redirect or click the buttons above or explore our latest blog posts below. +
+No posts found matching that search
+ :