mirror of
https://github.com/beilunyang/moemail.git
synced 2026-07-12 16:11:20 +08:00
feat: profile page & webhook notification
This commit is contained in:
77
app/components/profile/profile-card.tsx
Normal file
77
app/components/profile/profile-card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
155
app/components/profile/webhook-config.tsx
Normal file
155
app/components/profile/webhook-config.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user