update(env):more env
This commit is contained in:
parent
95616fa037
commit
e3fd88980a
@ -1 +1,2 @@
|
||||
NEXT_PUBLIC_BASE_URL=https://example.net
|
||||
NEXT_PUBLIC_SUPPORT_EMAIL=example@example.net
|
@ -20,7 +20,7 @@ RUN mkdir -p /app/data && chmod 755 /app/data
|
||||
WORKDIR /app
|
||||
RUN sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirrors.tuna.tsinghua.edu.cn/alpine#g' /etc/apk/repositories
|
||||
RUN apk update
|
||||
ENV NODE_ENV Production
|
||||
ENV NODE_ENV=Production
|
||||
COPY --from=builder /app/next.config.js ./
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
23
README.md
23
README.md
@ -8,28 +8,29 @@
|
||||
</div>
|
||||
|
||||
### 部署
|
||||
|
||||
支持部署环境:
|
||||
|
||||
- Docker
|
||||
- Systemd
|
||||
|
||||
自动(推荐):
|
||||
```bash
|
||||
curl -sSL https://git.mei.lv/mei/short-url/raw/branch/main/auto.sh -o auto.sh && bash auto.sh
|
||||
```
|
||||
|
||||
手动:
|
||||
```shell
|
||||
git clone https://git.mei.lv/mei/short-url.git
|
||||
cd short-url
|
||||
mkdir url-shortener
|
||||
cd url-shortener
|
||||
mkdir data
|
||||
touch .env ## 参考 .env.example 填写
|
||||
docker build -t url-shortener:latest .
|
||||
mkdir /opt/url-shortener
|
||||
cd /opt/url-shortener
|
||||
wget https://git.mei.lv/mei/short-url/raw/branch/main/docker-compose.yaml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
部署成功后,服务会在 `8567` 端口上启动
|
||||
|
||||
### 迁移
|
||||
|
||||
替换 `data/` 目录下的 `shorturl.db` 文件即可
|
||||
|
||||
### 开发
|
||||
|
||||
```shell
|
||||
mkdir data
|
||||
```
|
||||
|
23
app/page.tsx
23
app/page.tsx
@ -1,23 +1,30 @@
|
||||
import ShortUrlForm from '@/components/ShortUrlForm'
|
||||
import ShortUrlForm from "@/components/ShortUrlForm";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-r from-cyan-500 to-blue-500 p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<h1 className="mb-8 text-center text-4xl font-bold text-white">URL Shortener</h1>
|
||||
<h1 className="mb-8 text-center text-4xl font-bold text-white">
|
||||
URL Shortener
|
||||
</h1>
|
||||
<ShortUrlForm />
|
||||
<div className="mt-8 text-center text-sm text-white">
|
||||
<p>如果您需要云服务器,欢迎使用<a href="https://www.rainyun.com/cat_">雨云</a>,性价比超高!</p>
|
||||
<p>
|
||||
如果您需要云服务器,欢迎使用
|
||||
<a href="https://www.rainyun.com/cat_">雨云</a>,性价比超高!
|
||||
</p>
|
||||
<p>注册时填写优惠码 cat ,享更多优惠!</p>
|
||||
<p className="mt-2">
|
||||
违规举报:{' '}
|
||||
<a href="mailto:i@mei.lv" className="underline">
|
||||
i@mei.lv
|
||||
违规举报:{" "}
|
||||
<a
|
||||
href={`mailto:${process.env.NEXT_PUBLIC_SUPPORT_EMAIL}`}
|
||||
className="text-red-500 hover:underline"
|
||||
>
|
||||
{process.env.NEXT_PUBLIC_SUPPORT_EMAIL}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,68 +1,85 @@
|
||||
'use client'
|
||||
"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, ExternalLink } from 'lucide-react'
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
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,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
interface UrlData {
|
||||
longUrl: string
|
||||
expired: boolean
|
||||
longUrl: string;
|
||||
expired: boolean;
|
||||
stats: {
|
||||
visits: number
|
||||
expiresAt: string | null
|
||||
}
|
||||
visits: number;
|
||||
expiresAt: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export default function RedirectPage({ params }: { params: { shortCode: string } }) {
|
||||
const [data, setData] = useState<UrlData | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
export default function RedirectPage({
|
||||
params,
|
||||
}: {
|
||||
params: { shortCode: string };
|
||||
}) {
|
||||
const [data, setData] = useState<UrlData | null>(null);
|
||||
const [error, setError] = useState<string | null>(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()
|
||||
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
|
||||
setError(json.error || "Failed to fetch URL data");
|
||||
return;
|
||||
}
|
||||
|
||||
setData(json)
|
||||
setData(json);
|
||||
} catch (err) {
|
||||
setError('Failed to load URL data')
|
||||
console.error('Error fetching data:', err);
|
||||
setError("Failed to load URL data");
|
||||
console.error("Error fetching data:", err);
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchData()
|
||||
}, [params.shortCode])
|
||||
fetchData();
|
||||
}, [params.shortCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.longUrl) {
|
||||
const interval = setInterval(() => {
|
||||
setProgress(prev => {
|
||||
setProgress((prev) => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(interval)
|
||||
window.location.href = data.longUrl
|
||||
return 100
|
||||
clearInterval(interval);
|
||||
window.location.href = data.longUrl;
|
||||
return 100;
|
||||
}
|
||||
return prev + 2
|
||||
})
|
||||
}, 100)
|
||||
return prev + 2;
|
||||
});
|
||||
}, 100);
|
||||
|
||||
return () => clearInterval(interval)
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [data?.longUrl])
|
||||
}, [data?.longUrl]);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSkeleton />
|
||||
return <LoadingSkeleton />;
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
@ -71,7 +88,7 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl text-center text-red-600">
|
||||
{error || 'URL Not Found'}
|
||||
{error || "URL Not Found"}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@ -89,7 +106,7 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -107,9 +124,7 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
||||
<Card className="bg-blue-50 border-blue-200">
|
||||
<CardContent className="p-4">
|
||||
<Progress value={progress} className="w-full h-2 mb-2" />
|
||||
<p className="text-sm text-blue-600 text-center">
|
||||
少女祈祷中...
|
||||
</p>
|
||||
<p className="text-sm text-blue-600 text-center">少女祈祷中...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -118,7 +133,9 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
||||
<CardContent className="p-4 flex items-center space-x-2">
|
||||
<Link className="text-blue-600 w-5 h-5" />
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-700">我们的位置</p>
|
||||
<p className="text-sm font-semibold text-gray-700">
|
||||
我们的位置
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 break-all">{`${process.env.NEXT_PUBLIC_BASE_URL}/s/${params.shortCode}`}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -137,7 +154,9 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
||||
<ExternalLink className="text-purple-600 w-5 h-5" />
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-700">目的地</p>
|
||||
<p className="text-xs text-gray-500 truncate max-w-[180px]">{data.longUrl}</p>
|
||||
<p className="text-xs text-gray-500 truncate max-w-[180px]">
|
||||
{data.longUrl}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -145,9 +164,13 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
||||
<CardContent className="p-4 flex items-center space-x-2">
|
||||
<Clock className="text-orange-600 w-5 h-5" />
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-700">过期时间</p>
|
||||
<p className="text-sm font-semibold text-gray-700">
|
||||
过期时间
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{data.stats.expiresAt ? new Date(data.stats.expiresAt).toLocaleString() : 'Never'}
|
||||
{data.stats.expiresAt
|
||||
? new Date(data.stats.expiresAt).toLocaleString()
|
||||
: "Never"}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -158,7 +181,9 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<AlertTriangle className="text-yellow-600 w-5 h-5" />
|
||||
<CardTitle className="text-lg font-bold text-yellow-800">警告</CardTitle>
|
||||
<CardTitle className="text-lg font-bold text-yellow-800">
|
||||
警告
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@ -175,22 +200,30 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
||||
<CardContent>
|
||||
<p className="text-sm">
|
||||
雨云,高性价比的云服务商
|
||||
<a href="https://www.rainyun.com/cat_" className="underline ml-1 font-semibold">立即购买!</a>
|
||||
<a
|
||||
href="https://www.rainyun.com/cat_"
|
||||
className="underline ml-1 font-semibold"
|
||||
>
|
||||
立即购买!
|
||||
</a>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center">
|
||||
<p className="text-sm text-gray-500">
|
||||
违规举报:{' '}
|
||||
<a href="mailto:i@mei.lv" className="text-blue-500 hover:underline">
|
||||
i@mei.lv
|
||||
违规举报:{" "}
|
||||
<a
|
||||
href={`mailto:${process.env.NEXT_PUBLIC_SUPPORT_EMAIL}`}
|
||||
className="text-red-500 hover:underline"
|
||||
>
|
||||
{process.env.NEXT_PUBLIC_SUPPORT_EMAIL}
|
||||
</a>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingSkeleton() {
|
||||
@ -245,6 +278,5 @@ function LoadingSkeleton() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "short-url",
|
||||
"version": "0.1.0",
|
||||
"name": "url-shortener",
|
||||
"version": "v1.0.13",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
4073
pnpm-lock.yaml
Normal file
4073
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user