feats: add error short link page

This commit is contained in:
oiov
2025-06-29 17:54:18 +08:00
parent c102955cd5
commit d0ba3a1686
10 changed files with 295 additions and 11 deletions

View File

@@ -26,7 +26,7 @@ export async function POST(req: NextRequest) {
if (!slug || !ip) return Response.json("Missing[0000]");
const res = await getUrlBySuffix(slug);
if (!res) return Response.json("Disabled[0002]");
if (!res) return Response.json("Missing[0000]");
if (res.active !== 1) return Response.json("Disabled[0002]");

View File

@@ -0,0 +1,19 @@
import MaxWidthWrapper from "@/components/shared/max-width-wrapper";
interface ProtectedLayoutProps {
children: React.ReactNode;
}
export default async function LinkStatus({ children }: ProtectedLayoutProps) {
return (
<div className="relative flex h-screen w-full overflow-hidden">
<div className="flex flex-1 flex-col">
<main className="flex-1">
<MaxWidthWrapper className="flex max-h-screen max-w-full flex-col gap-4 px-0 lg:gap-6">
{children}
</MaxWidthWrapper>
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,127 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { ArrowLeft, Home, RefreshCw } from "lucide-react";
import { useTranslations } from "next-intl";
import { siteConfig } from "@/config/site";
import { Button } from "@/components/ui/button";
import BlurImage from "@/components/shared/blur-image";
import { Icons } from "@/components/shared/icons";
interface StatusConfig {
titleKey: string;
descriptionKey: string;
icon: keyof typeof Icons;
actionTextKey: string;
showRetry?: boolean;
}
const statusConfigs: Record<string, StatusConfig> = {
missing: {
titleKey: "linkNotExist",
descriptionKey: "linkNotExistDescription",
icon: "notFonudLink",
actionTextKey: "contactCreatorReactivate",
},
expired: {
titleKey: "linkExpired",
descriptionKey: "linkExpiredDescription",
icon: "expiredLink",
actionTextKey: "contactCreatorReactivate",
},
disabled: {
titleKey: "linkDisabled",
descriptionKey: "linkDisabledDescription",
icon: "disabledLink",
actionTextKey: "contactCreatorReactivate",
},
system: {
titleKey: "systemError",
descriptionKey: "systemErrorDescription",
icon: "systemErrorLink",
actionTextKey: "contactCreatorOrAdmin",
showRetry: true,
},
};
export default function LinkStatusContent() {
const t = useTranslations("Components");
const searchParams = useSearchParams();
const errorType = searchParams.get("error") || "system";
const slug = searchParams.get("slug") || "";
const config = statusConfigs[errorType] || statusConfigs.system;
const handleRetry = () => {
if (slug) {
window.location.href = `/${slug}`;
} else {
window.location.reload();
}
};
const Icon = Icons[config.icon];
return (
<>
<div className="grids mx-auto mt-6 flex h-screen max-w-lg flex-col items-center justify-center border-muted p-8 sm:rounded-lg sm:border sm:shadow-md">
<Icon className="mx-auto size-20" />
<h1 className="my-2 text-2xl font-bold text-neutral-900 dark:text-white">
{t(config.titleKey)}
</h1>
{slug && (
<div className="my-4 flex min-w-28 max-w-72 items-center justify-start rounded-md bg-neutral-100 p-3 dark:bg-neutral-800">
{/* <p className="text-sm text-neutral-600 dark:text-neutral-400">
<span className="font-mono text-neutral-800 dark:text-white">
/{slug}
</span>
</p> */}
{t(config.descriptionKey)}
{t(config.actionTextKey)}
</div>
)}
{/* <p className="mb-6 max-w-md text-neutral-600 dark:text-neutral-400">
{t(config.descriptionKey)}
{t(config.actionTextKey)}
</p> */}
<div className="flex w-full flex-col justify-center gap-3 sm:w-fit sm:flex-row">
<Link
href="/"
className="flex items-center justify-center rounded-md bg-neutral-800 px-4 py-2 text-white transition-colors hover:bg-neutral-900 dark:bg-neutral-800"
>
<Home className="mr-2 size-4" />
{t("backToHome")}
</Link>
<Button
className="h-9 w-full sm:h-12 sm:w-fit"
variant="outline"
onClick={() => window.history.back()}
>
<ArrowLeft className="mr-2 size-4" />
{t("goBack")}
</Button>
</div>
</div>
<footer className="z-10 mt-auto py-4 text-center text-sm font-semibold text-neutral-600 dark:text-neutral-500">
Powered by{" "}
<Link
className="hover:underline"
href={"https://wr.do"}
target="_blank"
style={{ fontFamily: "Bahamas Bold" }}
>
{siteConfig.name}
</Link>
</footer>
</>
);
}

View File

@@ -0,0 +1,9 @@
import { Skeleton } from "@/components/ui/skeleton";
export default function DashboardLoading() {
return (
<>
<Skeleton className="h-full w-full rounded-lg" />
</>
);
}

12
app/link-status/page.tsx Normal file
View File

@@ -0,0 +1,12 @@
import { constructMetadata } from "@/lib/utils";
import LinkStatusContent from "./link-status-content";
export const metadata = constructMetadata({
title: "Invalid Link",
description: "Meet some problems of your link",
});
export default async function Page() {
return <LinkStatusContent />;
}

View File

@@ -567,4 +567,91 @@ export const Icons = {
</defs>
</svg>
),
disabledLink: ({ ...props }: LucideProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l3 -3m2 -2l1 -1" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path d="M3 3l18 18" />
<path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>
),
notFonudLink: ({ ...props }: LucideProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 7v4a1 1 0 0 0 1 1h3" />
<path d="M7 7v10" />
<path d="M10 8v8a1 1 0 0 0 1 1h2a1 1 0 0 0 1 -1v-8a1 1 0 0 0 -1 -1h-2a1 1 0 0 0 -1 1z" />
<path d="M17 7v4a1 1 0 0 0 1 1h3" />
<path d="M21 7v10" />
</svg>
),
expiredLink: ({ ...props }: LucideProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 48 48"
{...props}
>
<title>expire</title>
<g id="Layer_2" data-name="Layer 2">
<g id="invisible_box" data-name="invisible box">
<rect width="48" height="48" fill="none" />
</g>
<g id="Q3_icons" data-name="Q3 icons">
<g>
<path d="M14.2,31.9h0a2,2,0,0,0-.9-2.9A11.8,11.8,0,0,1,6.1,16.8,12,12,0,0,1,16.9,6a12.1,12.1,0,0,1,11.2,5.6,2.3,2.3,0,0,0,2.3.9h0a2,2,0,0,0,1.1-3,15.8,15.8,0,0,0-15-7.4,16,16,0,0,0-4.8,30.6A2,2,0,0,0,14.2,31.9Z" />
<path d="M16.5,11.5v5h-5a2,2,0,0,0,0,4h9v-9a2,2,0,0,0-4,0Z" />
<path d="M45.7,43l-15-26a2,2,0,0,0-3.4,0l-15,26A2,2,0,0,0,14,46H44A2,2,0,0,0,45.7,43ZM29,42a2,2,0,1,1,2-2A2,2,0,0,1,29,42Zm2-8a2,2,0,0,1-4,0V26a2,2,0,0,1,4,0Z" />
</g>
</g>
</g>
</svg>
),
systemErrorLink: ({ ...props }: LucideProps) => (
<svg
id="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
{...props}
>
<defs></defs>
<title>data-error</title>
<circle cx="11" cy="8" r="1" />
<circle cx="11" cy="16" r="1" />
<circle cx="11" cy="24" r="1" />
<path d="M24,3H8A2,2,0,0,0,6,5V27a2,2,0,0,0,2,2h8V27H8V21H26V5A2,2,0,0,0,24,3Zm0,16H8V13H24Zm0-8H8V5H24Z" />
<polygon points="29.24 29.58 26.41 26.75 29.24 23.92 27.83 22.51 25 25.33 22.17 22.51 20.76 23.92 23.59 26.75 20.76 29.58 22.17 30.99 25 28.16 27.83 30.99 29.24 29.58" />
<rect
id="_Transparent_Rectangle_"
data-name="&lt;Transparent Rectangle&gt;"
className="fill-none"
width="32"
height="32"
/>
</svg>
),
};

View File

@@ -305,7 +305,22 @@
"Active": "Active",
"Inactive": "Inactive",
"Pending": "Pending",
"Rejected": "Rejected"
"Rejected": "Rejected",
"linkNotExist": "Link Not Found",
"linkNotExistDescription": "Sorry, the short link you are trying to access does not exist. It may have been deleted or never created.",
"linkExpired": "Link Expired",
"linkExpiredDescription": "This short link has expired and can no longer be used.",
"linkDisabled": "Link Disabled",
"linkDisabledDescription": "This short link has been disabled by its creator.",
"systemError": "System Error",
"systemErrorDescription": "An error occurred while processing your request. Please try again later.",
"contactCreatorReactivate": "Please contact the creator of this short link to reactivate or create a new short link",
"contactCreatorOrAdmin": "Please contact the creator of this short link or system administrator",
"shortLink": "Short Link",
"retry": "Retry",
"backToHome": "Back to Home",
"goBack": "Go Back",
"contactSupportIfError": "If you believe this is an error, please contact technical support"
},
"Landing": {
"settings": "Settings",

View File

@@ -305,7 +305,22 @@
"Active": "有效解析",
"Inactive": "无效解析",
"Pending": "审核中",
"Rejected": "已拒绝"
"Rejected": "已拒绝",
"linkNotExist": "链接不存在",
"linkNotExistDescription": "很抱歉,您访问的短链接不存在。可能已被删除或从未创建过。",
"linkExpired": "链接已过期",
"linkExpiredDescription": "这个短链接已经过期,无法继续使用。",
"linkDisabled": "链接已禁用",
"linkDisabledDescription": "这个短链接已被创建者禁用。",
"systemError": "系统错误",
"systemErrorDescription": "处理您的请求时发生了错误,请稍后重试。",
"contactCreatorReactivate": "请联系短链接创建者重新激活或创建新的短链接。",
"contactCreatorOrAdmin": "请联系短链接创建者或系统管理员。",
"shortLink": "短链接",
"retry": "重试",
"backToHome": "返回首页",
"goBack": "返回上页",
"contactSupportIfError": "如果您认为这是一个错误,请联系技术支持"
},
"Landing": {
"settings": "设置",

View File

@@ -13,10 +13,10 @@ export const config = {
const isVercel = process.env.VERCEL;
const redirectMap = {
"Missing[0000]": "/docs/short-urls#missing-links",
"Expired[0001]": "/docs/short-urls#expired-links",
"Disabled[0002]": "/docs/short-urls#disabled-links",
"Error[0003]": "/docs/short-urls#error-links",
"Missing[0000]": "/link-status?error=missing&slug=",
"Expired[0001]": "/link-status?error=expired&slug=",
"Disabled[0002]": "/link-status?error=disabled&slug=",
"Error[0003]": "/link-status?error=system&slug=",
"PasswordRequired[0004]": "/password-prompt?error=0&slug=",
"IncorrectPassword[0005]": "/password-prompt?error=1&slug=",
};
@@ -67,7 +67,7 @@ async function handleShortUrl(req: NextAuthRequest) {
if (!res.ok)
return NextResponse.redirect(
`${siteConfig.url}${redirectMap["Error[0003]"]}`,
`${siteConfig.url}${redirectMap["Error[0003]"]}${slug}`,
302,
);
@@ -75,7 +75,7 @@ async function handleShortUrl(req: NextAuthRequest) {
if (!target || typeof target !== "string") {
return NextResponse.redirect(
`${siteConfig.url}${redirectMap["Error[0003]"]}`,
`${siteConfig.url}${redirectMap["Error[0003]"]}${slug}`,
302,
);
}
@@ -91,7 +91,7 @@ async function handleShortUrl(req: NextAuthRequest) {
}
return NextResponse.redirect(
`${siteConfig.url}${redirectMap[target]}`,
`${siteConfig.url}${redirectMap[target]}${slug}`,
302,
);
}

File diff suppressed because one or more lines are too long