diff --git a/src/app/blog/[slug]/loading.tsx b/src/app/blog/[slug]/loading.tsx new file mode 100644 index 0000000..37aaae3 --- /dev/null +++ b/src/app/blog/[slug]/loading.tsx @@ -0,0 +1,5 @@ +import { DetailPageLoading } from "@/components/page-loading"; + +export default function Loading() { + return ; +} diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx index 65680d4..4c2561e 100644 --- a/src/app/blog/[slug]/page.tsx +++ b/src/app/blog/[slug]/page.tsx @@ -1,10 +1,10 @@ import type { Metadata } from "next"; -import Link from "next/link"; import { notFound } from "next/navigation"; import Markdown from "@/components/Markdown"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; +import { Link } from "@/lib/router"; import { PublicApiError, getPublicPost } from "@/lib/public-api"; import { apiBaseUrl, siteUrl, toAbsoluteUrl } from "@/lib/site"; import { formatJalali } from "@/lib/utils"; @@ -109,7 +109,7 @@ export default async function BlogDetailPage({
{formatJalali(post.published_at || post.created_at, false)} diff --git a/src/app/blog/loading.tsx b/src/app/blog/loading.tsx new file mode 100644 index 0000000..2b27efc --- /dev/null +++ b/src/app/blog/loading.tsx @@ -0,0 +1,5 @@ +import { ListingPageLoading } from "@/components/page-loading"; + +export default function Loading() { + return ; +} diff --git a/src/app/events/[slug]/loading.tsx b/src/app/events/[slug]/loading.tsx new file mode 100644 index 0000000..37aaae3 --- /dev/null +++ b/src/app/events/[slug]/loading.tsx @@ -0,0 +1,5 @@ +import { DetailPageLoading } from "@/components/page-loading"; + +export default function Loading() { + return ; +} diff --git a/src/app/events/loading.tsx b/src/app/events/loading.tsx new file mode 100644 index 0000000..2b27efc --- /dev/null +++ b/src/app/events/loading.tsx @@ -0,0 +1,5 @@ +import { ListingPageLoading } from "@/components/page-loading"; + +export default function Loading() { + return ; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 15814d7..6a254f6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,10 @@ import type { Metadata } from "next"; import localFont from "next/font/local"; +import { Suspense } from "react"; import Navbar from "@/components/Navbar"; import Footer from "@/components/Footer"; import Providers from "@/components/providers"; +import RouteProgress from "@/components/RouteProgress"; import { siteUrl } from "@/lib/site"; import "../index.css"; @@ -33,6 +35,9 @@ export default function RootLayout({ + + + {children}
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index a6b9631..86494cc 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,5 +1,5 @@ -import Link from "next/link"; import { Button } from "@/components/ui/button"; +import { Link } from "@/lib/router"; export default function NotFound() { return ( @@ -12,7 +12,7 @@ export default function NotFound() { آدرسی که وارد کرده‌اید وجود ندارد یا جابه‌جا شده است.

); diff --git a/src/components/RouteProgress.tsx b/src/components/RouteProgress.tsx new file mode 100644 index 0000000..109018c --- /dev/null +++ b/src/components/RouteProgress.tsx @@ -0,0 +1,120 @@ +"use client"; + +import * as React from "react"; +import { usePathname, useSearchParams } from "next/navigation"; +import { + completeNavigationProgress, + subscribeNavigationProgress, +} from "@/lib/navigation-progress"; + +const START_VALUE = 18; +const MAX_ACTIVE_VALUE = 90; + +export default function RouteProgress() { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const [visible, setVisible] = React.useState(false); + const [progress, setProgress] = React.useState(0); + const intervalRef = React.useRef(null); + const finishTimeoutRef = React.useRef(null); + const safetyTimeoutRef = React.useRef(null); + const activeRef = React.useRef(false); + const routeKey = `${pathname ?? ""}?${searchParams?.toString() ?? ""}`; + + const clearTimers = React.useCallback(() => { + if (intervalRef.current !== null) { + window.clearInterval(intervalRef.current); + intervalRef.current = null; + } + + if (finishTimeoutRef.current !== null) { + window.clearTimeout(finishTimeoutRef.current); + finishTimeoutRef.current = null; + } + + if (safetyTimeoutRef.current !== null) { + window.clearTimeout(safetyTimeoutRef.current); + safetyTimeoutRef.current = null; + } + }, []); + + const finish = React.useCallback(() => { + if (!activeRef.current) { + return; + } + + activeRef.current = false; + clearTimers(); + setProgress(100); + + finishTimeoutRef.current = window.setTimeout(() => { + setVisible(false); + setProgress(0); + }, 220); + }, [clearTimers]); + + const start = React.useCallback(() => { + clearTimers(); + activeRef.current = true; + setVisible(true); + setProgress(START_VALUE); + + window.requestAnimationFrame(() => { + setProgress(28); + }); + + intervalRef.current = window.setInterval(() => { + setProgress((current) => { + if (current >= MAX_ACTIVE_VALUE) { + return current; + } + + const delta = Math.max((MAX_ACTIVE_VALUE - current) * 0.14, 1.5); + return Math.min(MAX_ACTIVE_VALUE, current + delta); + }); + }, 180); + + safetyTimeoutRef.current = window.setTimeout(() => { + finish(); + }, 12000); + }, [clearTimers, finish]); + + React.useEffect(() => { + return subscribeNavigationProgress((event) => { + if (event === "start") { + start(); + return; + } + + finish(); + }); + }, [finish, start]); + + React.useEffect(() => { + finish(); + }, [finish, routeKey]); + + React.useEffect(() => { + return () => { + clearTimers(); + }; + }, [clearTimers]); + + return ( +