1st
This commit is contained in:
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
94
app/globals.css
Normal file
94
app/globals.css
Normal 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;
|
||||
}
|
||||
}
|
||||
120
app/gravatar/[...path]/route.ts
Normal file
120
app/gravatar/[...path]/route.ts
Normal 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
19
app/layout.tsx
Normal 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
264
app/page.tsx
Normal 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
21
app/random-image/route.ts
Normal 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user