feats: add error short link page
This commit is contained in:
@@ -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]");
|
||||
|
||||
|
||||
19
app/link-status/layout.tsx
Normal file
19
app/link-status/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
127
app/link-status/link-status-content.tsx
Normal file
127
app/link-status/link-status-content.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
9
app/link-status/loading.tsx
Normal file
9
app/link-status/loading.tsx
Normal 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
12
app/link-status/page.tsx
Normal 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 />;
|
||||
}
|
||||
@@ -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="<Transparent Rectangle>"
|
||||
className="fill-none"
|
||||
width="32"
|
||||
height="32"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "设置",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user