Compare commits
1 Commits
5711961b9b
...
42f2087b7c
| Author | SHA1 | Date | |
|---|---|---|---|
| 42f2087b7c |
@@ -1,5 +0,0 @@
|
|||||||
import { DetailPageLoading } from "@/components/page-loading";
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return <DetailPageLoading />;
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import Markdown from "@/components/Markdown";
|
import Markdown from "@/components/Markdown";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Link } from "@/lib/router";
|
|
||||||
import { PublicApiError, getPublicPost } from "@/lib/public-api";
|
import { PublicApiError, getPublicPost } from "@/lib/public-api";
|
||||||
import { apiBaseUrl, siteUrl, toAbsoluteUrl } from "@/lib/site";
|
import { apiBaseUrl, siteUrl, toAbsoluteUrl } from "@/lib/site";
|
||||||
import { formatJalali } from "@/lib/utils";
|
import { formatJalali } from "@/lib/utils";
|
||||||
@@ -109,7 +109,7 @@ export default async function BlogDetailPage({
|
|||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<div className="mb-6 flex items-center justify-between gap-3">
|
<div className="mb-6 flex items-center justify-between gap-3">
|
||||||
<Button variant="outline" asChild>
|
<Button variant="outline" asChild>
|
||||||
<Link to="/blog">بازگشت به وبلاگ</Link>
|
<Link href="/blog">بازگشت به وبلاگ</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{formatJalali(post.published_at || post.created_at, false)}
|
{formatJalali(post.published_at || post.created_at, false)}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { ListingPageLoading } from "@/components/page-loading";
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return <ListingPageLoading />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { DetailPageLoading } from "@/components/page-loading";
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return <DetailPageLoading />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { ListingPageLoading } from "@/components/page-loading";
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return <ListingPageLoading />;
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -1,21 +1,10 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import localFont from "next/font/local";
|
|
||||||
import { Suspense } from "react";
|
|
||||||
import Navbar from "@/components/Navbar";
|
import Navbar from "@/components/Navbar";
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
import Providers from "@/components/providers";
|
import Providers from "@/components/providers";
|
||||||
import RouteProgress from "@/components/RouteProgress";
|
|
||||||
import { siteUrl } from "@/lib/site";
|
import { siteUrl } from "@/lib/site";
|
||||||
import "../index.css";
|
import "../index.css";
|
||||||
|
|
||||||
const vazirmatn = localFont({
|
|
||||||
src: "./fonts/Vazirmatn-variable.woff2",
|
|
||||||
display: "swap",
|
|
||||||
weight: "100 900",
|
|
||||||
variable: "--font-vazirmatn",
|
|
||||||
fallback: ["Tahoma", "Arial", "sans-serif"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
metadataBase: new URL(siteUrl),
|
metadataBase: new URL(siteUrl),
|
||||||
title: {
|
title: {
|
||||||
@@ -33,11 +22,8 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="fa" dir="rtl" suppressHydrationWarning>
|
<html lang="fa" dir="rtl" suppressHydrationWarning>
|
||||||
<body className={`${vazirmatn.variable} font-sans antialiased`}>
|
<body>
|
||||||
<Providers>
|
<Providers>
|
||||||
<Suspense fallback={null}>
|
|
||||||
<RouteProgress />
|
|
||||||
</Suspense>
|
|
||||||
<Navbar />
|
<Navbar />
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Link } from "@/lib/router";
|
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
@@ -12,7 +12,7 @@ export default function NotFound() {
|
|||||||
آدرسی که وارد کردهاید وجود ندارد یا جابهجا شده است.
|
آدرسی که وارد کردهاید وجود ندارد یا جابهجا شده است.
|
||||||
</p>
|
</p>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link to="/">بازگشت به خانه</Link>
|
<Link href="/">بازگشت به خانه</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
"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<number | null>(null);
|
|
||||||
const finishTimeoutRef = React.useRef<number | null>(null);
|
|
||||||
const safetyTimeoutRef = React.useRef<number | null>(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 (
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="pointer-events-none fixed inset-x-0 top-0 z-[100] h-1 overflow-hidden"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`absolute inset-0 transition-opacity duration-200 ${
|
|
||||||
visible ? "opacity-100" : "opacity-0"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="h-full origin-right bg-[linear-gradient(90deg,hsl(var(--primary))_0%,hsl(var(--route-progress))_100%)] shadow-[0_0_16px_hsl(var(--route-progress)/0.55)] transition-[transform] duration-200 ease-out"
|
|
||||||
style={{ transform: `scaleX(${progress / 100})` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
|
|
||||||
export function ListingPageLoading() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-background" dir="rtl">
|
|
||||||
<div className="container mx-auto px-4 py-8">
|
|
||||||
<Skeleton className="mb-8 h-10 w-40" />
|
|
||||||
<Skeleton className="mb-8 h-10 w-full max-w-md" />
|
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{Array.from({ length: 6 }).map((_, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="overflow-hidden rounded-lg border bg-card"
|
|
||||||
>
|
|
||||||
<Skeleton className="aspect-video w-full rounded-none" />
|
|
||||||
<div className="space-y-4 p-6">
|
|
||||||
<div className="flex items-start justify-between gap-3">
|
|
||||||
<Skeleton className="h-6 flex-1" />
|
|
||||||
<Skeleton className="h-6 w-16" />
|
|
||||||
</div>
|
|
||||||
<Skeleton className="h-4 w-32" />
|
|
||||||
<div className="space-y-3 pt-2">
|
|
||||||
<Skeleton className="h-4 w-full" />
|
|
||||||
<Skeleton className="h-4 w-5/6" />
|
|
||||||
<Skeleton className="h-10 w-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DetailPageLoading() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-background" dir="rtl">
|
|
||||||
<div className="container mx-auto px-4 py-8">
|
|
||||||
<div className="mb-6 flex items-center justify-between gap-3">
|
|
||||||
<Skeleton className="h-10 w-36" />
|
|
||||||
<Skeleton className="h-5 w-28" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="overflow-hidden rounded-lg border bg-card">
|
|
||||||
<Skeleton className="aspect-video w-full rounded-none" />
|
|
||||||
<div className="space-y-6 p-6">
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<Skeleton className="h-6 w-20" />
|
|
||||||
<Skeleton className="h-6 w-24" />
|
|
||||||
</div>
|
|
||||||
<Skeleton className="h-10 w-4/5" />
|
|
||||||
<Skeleton className="h-4 w-40" />
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Skeleton className="h-4 w-full" />
|
|
||||||
<Skeleton className="h-4 w-full" />
|
|
||||||
<Skeleton className="h-4 w-11/12" />
|
|
||||||
<Skeleton className="h-4 w-5/6" />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
|
||||||
<Skeleton className="h-28 w-full" />
|
|
||||||
<Skeleton className="h-28 w-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: 'Vazirmatn', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
/* Definition of the design system. All colors, gradients, fonts, etc should be defined here.
|
/* Definition of the design system. All colors, gradients, fonts, etc should be defined here.
|
||||||
All colors MUST be HSL.
|
All colors MUST be HSL.
|
||||||
*/
|
*/
|
||||||
@@ -53,7 +57,6 @@ All colors MUST be HSL.
|
|||||||
--sidebar-border: 220 13% 91%;
|
--sidebar-border: 220 13% 91%;
|
||||||
|
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
--route-progress: 199 89% 48%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -92,7 +95,6 @@ All colors MUST be HSL.
|
|||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||||
--sidebar-border: 240 3.7% 15.9%;
|
--sidebar-border: 240 3.7% 15.9%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
--route-progress: 198 93% 60%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +104,6 @@ All colors MUST be HSL.
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background font-sans text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
type ProgressEvent = "start" | "done";
|
|
||||||
type Listener = (event: ProgressEvent) => void;
|
|
||||||
|
|
||||||
const listeners = new Set<Listener>();
|
|
||||||
let active = false;
|
|
||||||
|
|
||||||
function emit(event: ProgressEvent) {
|
|
||||||
listeners.forEach((listener) => listener(event));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function subscribeNavigationProgress(listener: Listener) {
|
|
||||||
listeners.add(listener);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
listeners.delete(listener);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function startNavigationProgress() {
|
|
||||||
if (active) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
active = true;
|
|
||||||
emit("start");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function completeNavigationProgress() {
|
|
||||||
if (!active) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
active = false;
|
|
||||||
emit("done");
|
|
||||||
}
|
|
||||||
@@ -7,10 +7,6 @@ import {
|
|||||||
usePathname,
|
usePathname,
|
||||||
useRouter,
|
useRouter,
|
||||||
} from "next/navigation";
|
} from "next/navigation";
|
||||||
import {
|
|
||||||
completeNavigationProgress,
|
|
||||||
startNavigationProgress,
|
|
||||||
} from "@/lib/navigation-progress";
|
|
||||||
|
|
||||||
type LinkProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
|
type LinkProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||||
to: string;
|
to: string;
|
||||||
@@ -30,53 +26,9 @@ type NavLinkProps = Omit<LinkProps, "className"> & {
|
|||||||
className?: string | ((state: { isActive: boolean }) => string);
|
className?: string | ((state: { isActive: boolean }) => string);
|
||||||
};
|
};
|
||||||
|
|
||||||
function isPlainLeftClick(event: React.MouseEvent<HTMLAnchorElement>) {
|
export function Link({ to, replace, prefetch, children, ...props }: LinkProps) {
|
||||||
return (
|
return (
|
||||||
event.button === 0 &&
|
<NextLink href={to} replace={replace} prefetch={prefetch} {...props}>
|
||||||
!event.metaKey &&
|
|
||||||
!event.ctrlKey &&
|
|
||||||
!event.shiftKey &&
|
|
||||||
!event.altKey
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldTrackNavigation(to: string) {
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const current = new URL(window.location.href);
|
|
||||||
const target = new URL(to, window.location.href);
|
|
||||||
return current.origin === target.origin && current.pathname !== target.pathname;
|
|
||||||
} catch {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Link({ to, replace, prefetch, children, onClick, target, ...props }: LinkProps) {
|
|
||||||
return (
|
|
||||||
<NextLink
|
|
||||||
href={to}
|
|
||||||
replace={replace}
|
|
||||||
prefetch={prefetch}
|
|
||||||
target={target}
|
|
||||||
onClick={(event) => {
|
|
||||||
onClick?.(event);
|
|
||||||
|
|
||||||
if (
|
|
||||||
event.defaultPrevented ||
|
|
||||||
target === "_blank" ||
|
|
||||||
!isPlainLeftClick(event) ||
|
|
||||||
!shouldTrackNavigation(to)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
startNavigationProgress();
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</NextLink>
|
</NextLink>
|
||||||
);
|
);
|
||||||
@@ -117,7 +69,6 @@ export function useNavigate(): NavigateFunction {
|
|||||||
return React.useCallback(
|
return React.useCallback(
|
||||||
(to: string | number, options?: { replace?: boolean }) => {
|
(to: string | number, options?: { replace?: boolean }) => {
|
||||||
if (typeof to === "number") {
|
if (typeof to === "number") {
|
||||||
startNavigationProgress();
|
|
||||||
if (to === -1) {
|
if (to === -1) {
|
||||||
router.back();
|
router.back();
|
||||||
return;
|
return;
|
||||||
@@ -128,12 +79,6 @@ export function useNavigate(): NavigateFunction {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldTrackNavigation(to)) {
|
|
||||||
startNavigationProgress();
|
|
||||||
} else {
|
|
||||||
completeNavigationProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.replace) {
|
if (options?.replace) {
|
||||||
router.replace(to);
|
router.replace(to);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default {
|
|||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["var(--font-vazirmatn)", "Tahoma", "Arial", "sans-serif"],
|
sans: ['Vazirmatn', 'sans-serif'],
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
border: "hsl(var(--border))",
|
border: "hsl(var(--border))",
|
||||||
|
|||||||
Reference in New Issue
Block a user