feat: profile page & webhook notification

This commit is contained in:
beilunyang
2024-12-17 13:26:34 +08:00
parent e0bd04818e
commit c69947ceae
20 changed files with 1533 additions and 288 deletions

View File

@@ -0,0 +1,77 @@
"use client"
import { User } from "next-auth"
import Image from "next/image"
import { Button } from "@/components/ui/button"
import { signOut } from "next-auth/react"
import { Github, Mail, Settings } from "lucide-react"
import { useRouter } from "next/navigation"
import { WebhookConfig } from "./webhook-config"
interface ProfileCardProps {
user: User
}
export function ProfileCard({ user }: ProfileCardProps) {
const router = useRouter()
return (
<div className="max-w-2xl mx-auto space-y-6">
{/* 用户信息卡片 */}
<div className="bg-background rounded-lg border-2 border-primary/20 p-6">
<div className="flex items-center gap-6">
<div className="relative">
{user.image && (
<Image
src={user.image}
alt={user.name || "用户头像"}
width={80}
height={80}
className="rounded-full ring-2 ring-primary/20"
/>
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h2 className="text-xl font-bold truncate">{user.name}</h2>
<div className="flex items-center gap-1 text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full">
<Github className="w-3 h-3" />
</div>
</div>
<p className="text-sm text-muted-foreground truncate mt-1">
{user.email}
</p>
</div>
</div>
</div>
{/* Webhook 配置卡片 */}
<div className="bg-background rounded-lg border-2 border-primary/20 p-6">
<div className="flex items-center gap-2 mb-6">
<Settings className="w-5 h-5 text-primary" />
<h2 className="text-lg font-semibold">Webhook </h2>
</div>
<WebhookConfig />
</div>
{/* 操作按钮 */}
<div className="flex flex-col sm:flex-row gap-4 px-1">
<Button
onClick={() => router.push("/moe")}
className="gap-2 flex-1"
>
<Mail className="w-4 h-4" />
</Button>
<Button
variant="outline"
onClick={() => signOut({ callbackUrl: "/" })}
className="flex-1"
>
退
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,155 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { useToast } from "@/components/ui/use-toast"
import { Loader2, Send } from "lucide-react"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
export function WebhookConfig() {
const [enabled, setEnabled] = useState(false)
const [url, setUrl] = useState("")
const [loading, setLoading] = useState(false)
const [testing, setTesting] = useState(false)
const { toast } = useToast()
useEffect(() => {
fetch("/api/webhook")
.then(res => res.json() as Promise<{ enabled: boolean; url: string }>)
.then(data => {
setEnabled(data.enabled)
setUrl(data.url)
})
.catch(console.error)
}, [])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!url) return
setLoading(true)
try {
const res = await fetch("/api/webhook", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url, enabled })
})
if (!res.ok) throw new Error("Failed to save")
toast({
title: "保存成功",
description: "Webhook 配置已更新"
})
} catch (_error) {
toast({
title: "保存失败",
description: "请稍后重试",
variant: "destructive"
})
} finally {
setLoading(false)
}
}
const handleTest = async () => {
if (!url) return
setTesting(true)
try {
const res = await fetch("/api/webhook/test", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url })
})
if (!res.ok) throw new Error("测试失败")
toast({
title: "测试成功",
description: "Webhook 调用成功,请检查目标服务器是否收到请求"
})
} catch (_error) {
toast({
title: "测试失败",
description: "请检查 URL 是否正确且可访问",
variant: "destructive"
})
} finally {
setTesting(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label> Webhook</Label>
<div className="text-sm text-muted-foreground">
URL
</div>
</div>
<Switch
checked={enabled}
onCheckedChange={setEnabled}
/>
</div>
{enabled && (
<div className="space-y-2">
<Label htmlFor="webhook-url">Webhook URL</Label>
<div className="flex gap-2">
<Input
id="webhook-url"
placeholder="https://example.com/webhook"
value={url}
onChange={(e) => setUrl(e.target.value)}
type="url"
required
/>
<Button type="submit" disabled={loading} className="w-20">
{loading ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
"保存"
)}
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="outline"
onClick={handleTest}
disabled={testing || !url}
>
{testing ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Send className="w-4 h-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p> Webhook</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<p className="text-xs text-muted-foreground">
URL POST ,
</p>
</div>
)}
</form>
)
}