diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/app/api/url/[shortCode]/route.ts b/app/api/url/[shortCode]/route.ts index 92eb6d3..1e71b8e 100644 --- a/app/api/url/[shortCode]/route.ts +++ b/app/api/url/[shortCode]/route.ts @@ -1,17 +1,27 @@ -import { NextResponse } from 'next/server'; -import { getLongUrl, getUrlStats } from '@/lib/db'; +import { NextResponse } from 'next/server' +import { getLongUrl, getUrlStats } from '@/lib/db' export async function GET( request: Request, { params }: { params: { shortCode: string } } ) { - const { longUrl, expired } = getLongUrl(params.shortCode); - const stats = getUrlStats(params.shortCode); + try { + const { longUrl, expired } = getLongUrl(params.shortCode) + const stats = getUrlStats(params.shortCode) - if (longUrl) { - return NextResponse.json({ longUrl, expired, stats }); - } else { - return NextResponse.json({ longUrl: null, expired, stats: null }, { status: 404 }); + if (!longUrl) { + return NextResponse.json( + { error: 'URL not found', expired }, + { status: 404 } + ) + } + + return NextResponse.json({ longUrl, expired, stats }) + } catch (error) { + console.error('Error fetching URL:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) } } - diff --git a/app/layout.tsx b/app/layout.tsx index a36cde0..acce447 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -14,8 +14,8 @@ const geistMono = localFont({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Url Shortener", + description: "缩短你的长链接!", }; export default function RootLayout({ @@ -24,7 +24,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + diff --git a/app/page.tsx b/app/page.tsx index 9c4ec43..54c5e58 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -7,11 +7,12 @@ export default function Home() {

URL Shortener

-

Short links now include visit tracking and expiration!

+

如果您需要云服务器,欢迎使用雨云,性价比超高!

+

注册时填写优惠码 cat ,享更多优惠!

- Report abusive content to:{' '} - - report@example.com + 违规举报:{' '} + + i@mei.lv

diff --git a/app/s/[shortCode]/page.tsx b/app/s/[shortCode]/page.tsx index 5d54843..5547545 100644 --- a/app/s/[shortCode]/page.tsx +++ b/app/s/[shortCode]/page.tsx @@ -1,130 +1,190 @@ -import { getLongUrl, getUrlStats } from '@/lib/db' -import Redirect from '@/components/Redirect' +'use client' + +import { useEffect, useState } from 'react' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Progress } from "@/components/ui/progress" -import { Clock, Link, BarChart2, AlertTriangle } from 'lucide-react' +import { Clock, Link, BarChart2, AlertTriangle, ExternalLink } from 'lucide-react' +import { Skeleton } from "@/components/ui/skeleton" -export default async function RedirectPage({ params }: { params: { shortCode: string } }) { - const { longUrl, expired } = getLongUrl(params.shortCode) - const stats = getUrlStats(params.shortCode) +interface UrlData { + longUrl: string + expired: boolean + stats: { + visits: number + expiresAt: string | null + } +} - if (longUrl && stats) { +export default function RedirectPage({ params }: { params: { shortCode: string } }) { + const [data, setData] = useState(null) + const [error, setError] = useState(null) + const [progress, setProgress] = useState(0) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch(`/api/url/${params.shortCode}`) + const json = await response.json() + + if (!response.ok) { + setError(json.error || 'Failed to fetch URL data') + return + } + + setData(json) + } catch (err) { + setError('Failed to load URL data') + console.error('Error fetching data:', err); + } finally { + setIsLoading(false) + } + } + + fetchData() + }, [params.shortCode]) + + useEffect(() => { + if (data?.longUrl) { + const interval = setInterval(() => { + setProgress(prev => { + if (prev >= 100) { + clearInterval(interval) + window.location.href = data.longUrl + return 100 + } + return prev + 2 + }) + }, 100) + + return () => clearInterval(interval) + } + }, [data?.longUrl]) + + if (isLoading) { + return + } + + if (error || !data) { return ( - <> - - - +
+ + + + {error || 'URL Not Found'} + + + +

+ 请求的短链接不存在或已过期。 +

+ +
+
+
) } - return ( -
- - - - {expired ? 'Link Expired' : 'Link Not Found'} - - - -

- {expired - ? 'Sorry, this short link has expired.' - : 'The requested short link does not exist.'} -

- -
- -

- Report abusive content:{' '} - - report@example.com - -

-
-
-
- ) -} - -function LandingPage({ longUrl, shortCode, visits, expiresAt }: { - longUrl: string - shortCode: string - visits: number - expiresAt: string | null -}) { return (
- You are being redirected + 您即将被传送... - You will be redirected to your destination in 5 seconds. + 您将在 {Math.ceil((100 - progress) / 20)} 秒后被传送! - -
-
- -

- Short URL: {`${process.env.NEXT_PUBLIC_BASE_URL}/s/${shortCode}`} + + + +

+ 少女祈祷中...

-
-
- -

- Visits: {visits} -

-
-
- -

- Destination: {longUrl} -

-
-
- -

- Expires at: {expiresAt ? new Date(expiresAt).toLocaleString() : 'Never'} -

-
-
-
-
- -

Disclaimer

-
-

- This link will take you to an external website. We are not responsible for the content - of external sites. Please proceed with caution. -

-
-
-

Advertisement

-

- This could be your ad! Contact us for advertising opportunities. -

+ + + +
+ + + +
+

我们的位置

+

{`${process.env.NEXT_PUBLIC_BASE_URL}/s/${params.shortCode}`}

+
+
+
+ + + +
+

访问量

+

{data.stats.visits}

+
+
+
+ + + +
+

目的地

+

{data.longUrl}

+
+
+
+ + + +
+

过期时间

+

+ {data.stats.expiresAt ? new Date(data.stats.expiresAt).toLocaleString() : 'Never'} +

+
+
+
+ + + +
+ + 警告 +
+
+ +

+ 本链接将会为您跳转一个外部网站,我们不对这些外部网站的内容负责,请谨慎访问。 +

+
+
+ + + + 推广 + + +

+ 雨云,高性价比的云服务商 + 立即购买! +

+
+

- Report abusive content:{' '} - - report@example.com + 违规举报:{' '} + + i@mei.lv

@@ -133,3 +193,58 @@ function LandingPage({ longUrl, shortCode, visits, expiresAt }: { ) } +function LoadingSkeleton() { + return ( +
+ + + + + + + + + + + + + +
+ {[...Array(4)].map((_, i) => ( + + + +
+ + +
+
+
+ ))} +
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ ) +} + diff --git a/components/Redirect.tsx b/components/Redirect.tsx index 3598f3e..4d8456a 100644 --- a/components/Redirect.tsx +++ b/components/Redirect.tsx @@ -1,29 +1,25 @@ 'use client' -import { useEffect, useState } from 'react' -import { Progress } from "@/components/ui/progress" +import { useEffect, useState, useCallback } from 'react' export default function Redirect({ url }: { url: string }) { const [progress, setProgress] = useState(0) - useEffect(() => { - const timer = setInterval(() => { - setProgress((oldProgress) => { - if (oldProgress === 100) { - clearInterval(timer) - window.location.href = url - return 100 - } - const diff = 100 / 50 // 50 steps for 5 seconds - return Math.min(oldProgress + diff, 100) - }) - }, 100) - - return () => clearInterval(timer) + const updateProgress = useCallback(() => { + setProgress(oldProgress => { + if (oldProgress >= 100) { + window.location.href = url + return 100 + } + return oldProgress + 2 // 每 100ms 增加 2% + }) }, [url]) - return ( - - ) + useEffect(() => { + const timer = setInterval(updateProgress, 100) + return () => clearInterval(timer) + }, [updateProgress]) + + return progress } diff --git a/components/ShortUrlForm.tsx b/components/ShortUrlForm.tsx index 27376c0..1215ad3 100644 --- a/components/ShortUrlForm.tsx +++ b/components/ShortUrlForm.tsx @@ -42,7 +42,7 @@ export default function ShortUrlForm() {
setLongUrl(e.target.value)} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" - placeholder="https://example.com/very/long/url" + placeholder="https://www.rainyun.com/cat_" />