update(env):more env

This commit is contained in:
mei 2025-06-02 09:54:40 +08:00
parent 95616fa037
commit e3fd88980a
7 changed files with 4199 additions and 85 deletions

View File

@ -1 +1,2 @@
NEXT_PUBLIC_BASE_URL=https://example.net
NEXT_PUBLIC_SUPPORT_EMAIL=example@example.net

View File

@ -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

View File

@ -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
```

View File

@ -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>
)
);
}

View File

@ -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>
)
);
}

View File

@ -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

File diff suppressed because it is too large Load Diff