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_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
|
WORKDIR /app
|
||||||
RUN sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirrors.tuna.tsinghua.edu.cn/alpine#g' /etc/apk/repositories
|
RUN sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirrors.tuna.tsinghua.edu.cn/alpine#g' /etc/apk/repositories
|
||||||
RUN apk update
|
RUN apk update
|
||||||
ENV NODE_ENV Production
|
ENV NODE_ENV=Production
|
||||||
COPY --from=builder /app/next.config.js ./
|
COPY --from=builder /app/next.config.js ./
|
||||||
COPY --from=builder /app/.next/standalone ./
|
COPY --from=builder /app/.next/standalone ./
|
||||||
COPY --from=builder /app/.next/static ./.next/static
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
|
25
README.md
25
README.md
@ -8,28 +8,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
### 部署
|
### 部署
|
||||||
|
|
||||||
支持部署环境:
|
支持部署环境:
|
||||||
|
|
||||||
- Docker
|
- Docker
|
||||||
- Systemd
|
- Systemd
|
||||||
|
|
||||||
自动(推荐):
|
|
||||||
```bash
|
|
||||||
curl -sSL https://git.mei.lv/mei/short-url/raw/branch/main/auto.sh -o auto.sh && bash auto.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
手动:
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://git.mei.lv/mei/short-url.git
|
mkdir url-shortener
|
||||||
cd short-url
|
cd url-shortener
|
||||||
|
mkdir data
|
||||||
touch .env ## 参考 .env.example 填写
|
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
|
wget https://git.mei.lv/mei/short-url/raw/branch/main/docker-compose.yaml
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
部署成功后,服务会在 `8567` 端口上启动
|
部署成功后,服务会在 `8567` 端口上启动
|
||||||
|
|
||||||
### 迁移
|
### 迁移
|
||||||
替换 `data/` 目录下的 `shorturl.db` 文件即可
|
|
||||||
|
替换 `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() {
|
export default function Home() {
|
||||||
return (
|
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">
|
<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">
|
<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 />
|
<ShortUrlForm />
|
||||||
<div className="mt-8 text-center text-sm text-white">
|
<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>注册时填写优惠码 cat ,享更多优惠!</p>
|
||||||
<p className="mt-2">
|
<p className="mt-2">
|
||||||
违规举报:{' '}
|
违规举报:{" "}
|
||||||
<a href="mailto:i@mei.lv" className="underline">
|
<a
|
||||||
i@mei.lv
|
href={`mailto:${process.env.NEXT_PUBLIC_SUPPORT_EMAIL}`}
|
||||||
|
className="text-red-500 hover:underline"
|
||||||
|
>
|
||||||
|
{process.env.NEXT_PUBLIC_SUPPORT_EMAIL}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,68 +1,85 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from "react";
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
import {
|
||||||
import { Progress } from "@/components/ui/progress"
|
Card,
|
||||||
import { Clock, Link, BarChart2, AlertTriangle, ExternalLink } from 'lucide-react'
|
CardContent,
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
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 {
|
interface UrlData {
|
||||||
longUrl: string
|
longUrl: string;
|
||||||
expired: boolean
|
expired: boolean;
|
||||||
stats: {
|
stats: {
|
||||||
visits: number
|
visits: number;
|
||||||
expiresAt: string | null
|
expiresAt: string | null;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RedirectPage({ params }: { params: { shortCode: string } }) {
|
export default function RedirectPage({
|
||||||
const [data, setData] = useState<UrlData | null>(null)
|
params,
|
||||||
const [error, setError] = useState<string | null>(null)
|
}: {
|
||||||
const [progress, setProgress] = useState(0)
|
params: { shortCode: string };
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
}) {
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/url/${params.shortCode}`)
|
const response = await fetch(`/api/url/${params.shortCode}`);
|
||||||
const json = await response.json()
|
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()
|
if (!response.ok) {
|
||||||
}, [params.shortCode])
|
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(() => {
|
useEffect(() => {
|
||||||
if (data?.longUrl) {
|
if (data?.longUrl) {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setProgress(prev => {
|
setProgress((prev) => {
|
||||||
if (prev >= 100) {
|
if (prev >= 100) {
|
||||||
clearInterval(interval)
|
clearInterval(interval);
|
||||||
window.location.href = data.longUrl
|
window.location.href = data.longUrl;
|
||||||
return 100
|
return 100;
|
||||||
}
|
}
|
||||||
return prev + 2
|
return prev + 2;
|
||||||
})
|
});
|
||||||
}, 100)
|
}, 100);
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval);
|
||||||
}
|
}
|
||||||
}, [data?.longUrl])
|
}, [data?.longUrl]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoadingSkeleton />
|
return <LoadingSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !data) {
|
if (error || !data) {
|
||||||
@ -71,7 +88,7 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
|||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl text-center text-red-600">
|
<CardTitle className="text-2xl text-center text-red-600">
|
||||||
{error || 'URL Not Found'}
|
{error || "URL Not Found"}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@ -89,7 +106,7 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -107,9 +124,7 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
|||||||
<Card className="bg-blue-50 border-blue-200">
|
<Card className="bg-blue-50 border-blue-200">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<Progress value={progress} className="w-full h-2 mb-2" />
|
<Progress value={progress} className="w-full h-2 mb-2" />
|
||||||
<p className="text-sm text-blue-600 text-center">
|
<p className="text-sm text-blue-600 text-center">少女祈祷中...</p>
|
||||||
少女祈祷中...
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -118,7 +133,9 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
|||||||
<CardContent className="p-4 flex items-center space-x-2">
|
<CardContent className="p-4 flex items-center space-x-2">
|
||||||
<Link className="text-blue-600 w-5 h-5" />
|
<Link className="text-blue-600 w-5 h-5" />
|
||||||
<div>
|
<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>
|
<p className="text-xs text-gray-500 break-all">{`${process.env.NEXT_PUBLIC_BASE_URL}/s/${params.shortCode}`}</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -137,7 +154,9 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
|||||||
<ExternalLink className="text-purple-600 w-5 h-5" />
|
<ExternalLink className="text-purple-600 w-5 h-5" />
|
||||||
<div>
|
<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 truncate max-w-[180px]">{data.longUrl}</p>
|
<p className="text-xs text-gray-500 truncate max-w-[180px]">
|
||||||
|
{data.longUrl}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -145,9 +164,13 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
|||||||
<CardContent className="p-4 flex items-center space-x-2">
|
<CardContent className="p-4 flex items-center space-x-2">
|
||||||
<Clock className="text-orange-600 w-5 h-5" />
|
<Clock className="text-orange-600 w-5 h-5" />
|
||||||
<div>
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -158,7 +181,9 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
|||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<AlertTriangle className="text-yellow-600 w-5 h-5" />
|
<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>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@ -175,22 +200,30 @@ export default function RedirectPage({ params }: { params: { shortCode: string }
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-sm">
|
<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>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="justify-center">
|
<CardFooter className="justify-center">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
违规举报:{' '}
|
违规举报:{" "}
|
||||||
<a href="mailto:i@mei.lv" className="text-blue-500 hover:underline">
|
<a
|
||||||
i@mei.lv
|
href={`mailto:${process.env.NEXT_PUBLIC_SUPPORT_EMAIL}`}
|
||||||
|
className="text-red-500 hover:underline"
|
||||||
|
>
|
||||||
|
{process.env.NEXT_PUBLIC_SUPPORT_EMAIL}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LoadingSkeleton() {
|
function LoadingSkeleton() {
|
||||||
@ -245,6 +278,5 @@ function LoadingSkeleton() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "short-url",
|
"name": "url-shortener",
|
||||||
"version": "0.1.0",
|
"version": "v1.0.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"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