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

View File

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

View File

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

View File

@ -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) { if (!response.ok) {
setError(json.error || 'Failed to fetch URL data') setError(json.error || "Failed to fetch URL data");
return return;
} }
setData(json) setData(json);
} catch (err) { } catch (err) {
setError('Failed to load URL data') setError("Failed to load URL data");
console.error('Error fetching data:', err); console.error("Error fetching data:", err);
} finally { } finally {
setIsLoading(false) setIsLoading(false);
}
} }
};
fetchData() fetchData();
}, [params.shortCode]) }, [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>
) );
} }

View File

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

File diff suppressed because it is too large Load Diff