This commit is contained in:
He
2025-03-25 21:47:40 +08:00
commit dc2b36db5d
73 changed files with 6154 additions and 0 deletions

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

94
app/globals.css Normal file
View File

@@ -0,0 +1,94 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: Arial, Helvetica, sans-serif;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,120 @@
import { type NextRequest, NextResponse } from "next/server"
// Replace with your mirror site's domain
const upstream = "gravatar.com"
// If it's a mobile-specific site, otherwise keep it the same as upstream
const upstream_mobile = "gravatar.com"
// Countries you wish to block from accessing
const blocked_region: string[] = []
// IP addresses you wish to block from accessing
const blocked_ip_address: string[] = []
// Replace the domains in the text response
const replace_dict: Record<string, string> = {
$upstream: "$custom_domain",
"//gravatar.com": "",
}
export async function GET(request: NextRequest, { params }: { params: { path: string[] } }) {
const url = new URL(request.url)
const url_host = url.host
const user_agent = request.headers.get("user-agent") || ""
// Determine if it's a mobile device
const is_desktop = await device_status(user_agent)
const upstream_domain = is_desktop ? upstream : upstream_mobile
// Construct the upstream URL
const path = params.path.join("/")
const upstream_url = `https://${upstream_domain}/${path}${url.search}`
try {
// Fetch from upstream
const upstream_response = await fetch(upstream_url, {
headers: {
Host: upstream_domain,
"User-Agent": user_agent,
Referer: url.href,
},
})
// Clone the response so we can read it multiple times
const original_response = upstream_response.clone()
// Get the response headers
const response_headers = new Headers(upstream_response.headers)
// Set CORS headers
response_headers.set("access-control-allow-origin", "*")
response_headers.set("access-control-allow-credentials", "true")
// Remove security headers that might cause issues
response_headers.delete("content-security-policy")
response_headers.delete("content-security-policy-report-only")
response_headers.delete("clear-site-data")
// Get the content type
const content_type = response_headers.get("content-type") || ""
// Process the response body
let response_body
if (content_type.includes("text/html") && content_type.includes("UTF-8")) {
// If it's HTML, replace text
const text = await original_response.text()
response_body = replace_response_text(text, upstream_domain, url_host)
} else {
// Otherwise, just pass through the body
response_body = original_response.body
}
// Create and return the new response
return new NextResponse(response_body, {
status: upstream_response.status,
headers: response_headers,
})
} catch (error) {
console.error("Gravatar proxy error:", error)
return new NextResponse("Error proxying to Gravatar", { status: 500 })
}
}
// Helper function to determine if the request is from a desktop device
function device_status(user_agent_info: string): boolean {
const agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"]
let flag = true
for (let v = 0; v < agents.length; v++) {
if (user_agent_info.indexOf(agents[v]) > 0) {
flag = false
break
}
}
return flag
}
// Helper function to replace text in the response
function replace_response_text(text: string, upstream_domain: string, host_name: string): string {
for (const [i, j] of Object.entries(replace_dict)) {
let from = i
let to = j
if (from === "$upstream") {
from = upstream_domain
} else if (from === "$custom_domain") {
from = host_name
}
if (to === "$upstream") {
to = upstream_domain
} else if (to === "$custom_domain") {
to = host_name
}
const re = new RegExp(from, "g")
text = text.replace(re, to)
}
return text
}

19
app/layout.tsx Normal file
View File

@@ -0,0 +1,19 @@
import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: 'Spircape API',
description: 'Spircape API',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

264
app/page.tsx Normal file
View File

@@ -0,0 +1,264 @@
"use client"
import { useState } from "react"
import { Copy, CheckCheck, ExternalLink, ImageIcon, UserCircle, Code } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { AlertTriangle } from "lucide-react"
import { Badge } from "@/components/ui/badge"
export default function Home() {
const [copied, setCopied] = useState<string | null>(null)
const baseUrl = typeof window !== "undefined" ? window.location.origin : ""
const randomImageUrl = `${baseUrl}/random-image`
const gravatarProxyUrl = `${baseUrl}/gravatar`
const copyToClipboard = (text: string, id: string) => {
navigator.clipboard.writeText(text)
setCopied(id)
setTimeout(() => setCopied(null), 2000)
}
return (
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-slate-100 dark:from-slate-950 dark:to-slate-900">
<div className="container mx-auto px-4 py-12">
<header className="text-center mb-16 pt-8">
<h1 className="text-4xl md:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-600 mb-4">
API Services
</h1>
<p className="text-lg text-slate-600 dark:text-slate-400 max-w-2xl mx-auto">
Connect the world with us
</p>
</header>
<Alert
variant="destructive"
className="mb-12 max-w-4xl mx-auto bg-red-50 border border-red-200 text-red-800 dark:bg-red-950 dark:border-red-800 dark:text-red-200"
>
<AlertTriangle className="h-5 w-5 mr-2 text-red-600 dark:text-red-400" />
<AlertTitle className="font-medium">Access Restricted</AlertTitle>
<AlertDescription className="text-sm md:text-base">
The API is not open to the public. If you need it, please contact us at{" "}
<a
href="mailto:10010@spircape.com"
className="font-medium underline hover:text-red-600 dark:hover:text-red-300 transition-colors"
>
10010@spircape.com
</a>
</AlertDescription>
</Alert>
<Card className="border-none shadow-lg mb-8">
<CardHeader className="bg-gradient-to-r from-primary/10 to-blue-500/10 border-b">
<CardTitle className="flex items-center gap-2">
<Code className="h-5 w-5" />
Integration Guide
</CardTitle>
<CardDescription>Follow these examples to integrate our services into your application</CardDescription>
</CardHeader>
<CardContent className="pt-6">
<Tabs defaultValue="random-image">
<TabsList className="mb-6 grid w-full grid-cols-2">
<TabsTrigger value="random-image" className="flex items-center gap-2">
<ImageIcon className="h-4 w-4" />
Random Image
</TabsTrigger>
<TabsTrigger value="gravatar" className="flex items-center gap-2">
<UserCircle className="h-4 w-4" />
Gravatar Proxy
</TabsTrigger>
</TabsList>
<TabsContent value="random-image" className="space-y-8">
<div className="space-y-4">
<div className="flex items-center gap-2">
<h3 className="text-xl font-medium">Random Image API</h3>
<Badge variant="outline" className="bg-primary/10 text-primary border-primary/20">
GET
</Badge>
</div>
<p className="text-slate-600 dark:text-slate-400">
This API randomly serves an image from a predefined collection. Perfect for placeholder images,
random backgrounds, or testing purposes.
</p>
<div className="space-y-6 mt-8">
<div>
<h4 className="font-medium flex items-center gap-2 mb-3">
<Code className="h-4 w-4 text-primary" />
HTML Usage
</h4>
<div className="relative">
<pre className="bg-slate-950 text-slate-100 p-4 rounded-md overflow-x-auto">
<code>{`<img src="${randomImageUrl}" alt="Random image" />`}</code>
</pre>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2 text-slate-400 hover:text-white hover:bg-slate-800"
onClick={() =>
copyToClipboard(`<img src="${randomImageUrl}" alt="Random image" />`, "html-random")
}
>
{copied === "html-random" ? <CheckCheck className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<div>
<h4 className="font-medium flex items-center gap-2 mb-3">
<Code className="h-4 w-4 text-primary" />
React Usage
</h4>
<div className="relative">
<pre className="bg-slate-950 text-slate-100 p-4 rounded-md overflow-x-auto">
<code>{`import { useState } from 'react';\n\nfunction RandomImage() {\n const [refresh, setRefresh] = useState(0);\n \n return (\n <img \n src={\`${randomImageUrl || "/placeholder.svg"}?t=\${refresh}\`} \n alt="Random image" \n onClick={() => setRefresh(Date.now())}\n className="cursor-pointer transition-opacity hover:opacity-90"\n />\n );\n}`}</code>
</pre>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2 text-slate-400 hover:text-white hover:bg-slate-800"
onClick={() =>
copyToClipboard(
`import { useState } from 'react';\n\nfunction RandomImage() {\n const [refresh, setRefresh] = useState(0);\n \n return (\n <img \n src={\`${randomImageUrl || "/placeholder.svg"}?t=\${refresh}\`} \n alt="Random image" \n onClick={() => setRefresh(Date.now())}\n className="cursor-pointer transition-opacity hover:opacity-90"\n />\n );\n}`,
"react-random",
)
}
>
{copied === "react-random" ? (
<CheckCheck className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="gravatar" className="space-y-8">
<div className="space-y-4">
<div className="flex items-center gap-2">
<h3 className="text-xl font-medium">Gravatar Proxy</h3>
<Badge variant="outline" className="bg-primary/10 text-primary border-primary/20">
GET
</Badge>
</div>
<p className="text-slate-600 dark:text-slate-400">
Our Gravatar proxy service improves loading speed and reliability for Gravatar images. It caches and
optimizes avatar delivery for your applications.
</p>
<div className="space-y-6 mt-8">
<div>
<h4 className="font-medium flex items-center gap-2 mb-3">
<Code className="h-4 w-4 text-primary" />
Basic Usage
</h4>
<div className="relative">
<pre className="bg-slate-950 text-slate-100 p-4 rounded-md overflow-x-auto">
<code>{`<!-- Replace [EMAIL_HASH] with MD5 hash of the email -->\n<img src="${gravatarProxyUrl}/avatar/[EMAIL_HASH]" alt="User avatar" />`}</code>
</pre>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2 text-slate-400 hover:text-white hover:bg-slate-800"
onClick={() =>
copyToClipboard(
`<!-- Replace [EMAIL_HASH] with MD5 hash of the email -->\n<img src="${gravatarProxyUrl}/avatar/[EMAIL_HASH]" alt="User avatar" />`,
"html-gravatar",
)
}
>
{copied === "html-gravatar" ? (
<CheckCheck className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
<div>
<h4 className="font-medium flex items-center gap-2 mb-3">
<Code className="h-4 w-4 text-primary" />
With Size Parameter
</h4>
<div className="relative">
<pre className="bg-slate-950 text-slate-100 p-4 rounded-md overflow-x-auto">
<code>{`<!-- Set size to 200px -->\n<img src="${gravatarProxyUrl}/avatar/[EMAIL_HASH]?s=200" alt="User avatar" />`}</code>
</pre>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2 text-slate-400 hover:text-white hover:bg-slate-800"
onClick={() =>
copyToClipboard(
`<!-- Set size to 200px -->\n<img src="${gravatarProxyUrl}/avatar/[EMAIL_HASH]?s=200" alt="User avatar" />`,
"size-gravatar",
)
}
>
{copied === "size-gravatar" ? (
<CheckCheck className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
<div>
<h4 className="font-medium flex items-center gap-2 mb-3">
<Code className="h-4 w-4 text-primary" />
With Default Image
</h4>
<div className="relative">
<pre className="bg-slate-950 text-slate-100 p-4 rounded-md overflow-x-auto">
<code>{`<!-- Use 'identicon' as default if email has no Gravatar -->\n<img src="${gravatarProxyUrl}/avatar/[EMAIL_HASH]?d=identicon" alt="User avatar" />`}</code>
</pre>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2 text-slate-400 hover:text-white hover:bg-slate-800"
onClick={() =>
copyToClipboard(
`<!-- Use 'identicon' as default if email has no Gravatar -->\n<img src="${gravatarProxyUrl}/avatar/[EMAIL_HASH]?d=identicon" alt="User avatar" />`,
"default-gravatar",
)
}
>
{copied === "default-gravatar" ? (
<CheckCheck className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
</div>
</div>
</TabsContent>
</Tabs>
</CardContent>
<CardFooter className="border-t bg-slate-50 dark:bg-slate-900 py-4">
<div className="flex items-center gap-2 text-sm text-slate-600 dark:text-slate-400">
<ExternalLink className="h-4 w-4" />
<span>The above is for example only, please adjust according to the specific development environment</span>
</div>
</CardFooter>
</Card>
<footer className="text-center text-slate-500 text-sm py-4 border-t border-slate-200 dark:border-slate-800">
<div className="flex justify-center items-center">
<p>© {new Date().getFullYear()} Spircape. All rights reserved.</p>
</div>
</footer>
</div>
</div>
)
}

21
app/random-image/route.ts Normal file
View File

@@ -0,0 +1,21 @@
import { type NextRequest, NextResponse } from "next/server"
export async function GET(request: NextRequest) {
// List of image URLs
const imageUrls = [
"https://zh.yuazhi.cn/apipng/18CD3BE92227887D576B2D4B7A9C9960.jpg",
"https://zh.yuazhi.cn/apipng/1.jpg",
"https://zh.yuazhi.cn/apipng/2.jpg",
"https://zh.yuazhi.cn/apipng/3.jpg",
"https://zh.yuazhi.cn/apipng/4.jpg",
"https://zh.yuazhi.cn/apipng/5.jpg",
]
// Select a random image URL
const randomIndex = Math.floor(Math.random() * imageUrls.length)
const randomImageUrl = imageUrls[randomIndex]
// Redirect to the random image
return NextResponse.redirect(randomImageUrl)
}