mirror of
https://github.com/woodchen-ink/Q58Connect.git
synced 2025-07-17 21:41:55 +08:00
feat: 添加代码块组件和警告组件,优化用户界面
This commit is contained in:
parent
ce3baad450
commit
54b3d8e661
50
README.md
50
README.md
@ -63,55 +63,7 @@ Q58论坛网址: https://q58.club
|
||||
|
||||
## 用户应用接入本系统oauth2.0认证的方式:
|
||||
|
||||
1. 发起授权请求
|
||||
将用户重定向到授权页面
|
||||
|
||||
const authUrl = 'https://connect.q58.club/oauth/authorize?' +
|
||||
new URLSearchParams({
|
||||
response_type: 'code', // 必填,固定值
|
||||
client_id: 'your_client_id', // 必填,您的应用ID
|
||||
redirect_uri: 'https://your-app.com/callback',
|
||||
state: 'random_state', // 建议提供,防CSRF攻击
|
||||
scope: 'read_profile' // 可选,默认read_profile
|
||||
});
|
||||
|
||||
window.location.href = authUrl;
|
||||
|
||||
2. 处理授权回调
|
||||
在回调地址处理授权结果
|
||||
|
||||
// 获取访问令牌
|
||||
const response = await fetch('https://connect.q58.club/api/oauth/access_token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
code: '授权码', // 回调参数中的code
|
||||
redirect_uri: 'https://your-app.com/callback'
|
||||
})
|
||||
});
|
||||
|
||||
const { access_token, expires_in } = await response.json();
|
||||
|
||||
3. 获取用户信息
|
||||
使用访问令牌获取用户数据
|
||||
|
||||
const userInfo = await fetch('https://connect.q58.club/api/oauth/user', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${access_token}`
|
||||
}
|
||||
}).then(res => res.json());
|
||||
|
||||
// 返回数据示例:
|
||||
{
|
||||
"id": "user_xxx",
|
||||
"email": "user@example.com",
|
||||
"username": "username",
|
||||
"name": "用户昵称",
|
||||
"avatar_url": "https://...",
|
||||
"groups": ["group1", "group2"]
|
||||
}
|
||||
写在"src\app\()\page.tsx"里了.
|
||||
|
||||
## 添加新功能的准则
|
||||
|
||||
|
75
oauth2.0的授权模式.txt
Normal file
75
oauth2.0的授权模式.txt
Normal file
@ -0,0 +1,75 @@
|
||||
授权码模式(Authorization Code)
|
||||
第一步:获取授权码
|
||||
|
||||
授权码请求链接格式:
|
||||
|
||||
http://localhost:8080/oauth/authorize?client_id=123456&response_type=code&scope=all&redirect_url=http://localhost:8080/oauth/token
|
||||
返回的JSON格式:
|
||||
|
||||
{
|
||||
"code": "123456",
|
||||
"state": "123456"
|
||||
}
|
||||
第二步:申请令牌
|
||||
|
||||
令牌请求链接格式:
|
||||
|
||||
http://localhost:8080/oauth/token?client_id=123456&client_secret=123456&grant_type=authorization_code&code=123456&redirect_url=http://localhost:8080/oauth/callback
|
||||
返回的JSON格式:
|
||||
|
||||
{
|
||||
"access_token": "123456",
|
||||
"token_type": "bearer",
|
||||
"scope": "read",
|
||||
"refresh_token": "123456"
|
||||
}
|
||||
资源请求链接格式:
|
||||
|
||||
http://localhost:8080/oauth/resource?access_token=123456
|
||||
返回的JSON格式:
|
||||
|
||||
{
|
||||
"resource": "123456"
|
||||
}
|
||||
简化模式(Implicit)
|
||||
简化模式跳过授权码,直接获取访问令牌,适用于没有后台服务程序的单页面应用。
|
||||
|
||||
令牌请求链接格式:
|
||||
|
||||
http://localhost:8080/oauth/token?client_id=123456&client_secret=123456&response_type=token&scope=all&redirect_url=http://localhost:8080/oauth/callback
|
||||
返回的JSON格式:
|
||||
|
||||
{
|
||||
"access_token": "123456",
|
||||
"token_type": "bearer",
|
||||
"scope": "read",
|
||||
"refresh_token": "123456"
|
||||
}
|
||||
密码模式(Password)
|
||||
用户通过客户端使用用户名和密码向授权服务器请求授权,授权服务器向客户端发送访问令牌和更新令牌。
|
||||
|
||||
请求链接格式:
|
||||
|
||||
http://localhost:8080/oauth/token?client_id=123456&client_secret=123456&grant_type=password&username=admin&password=admin
|
||||
返回的JSON格式:
|
||||
|
||||
{
|
||||
"access_token": "123456",
|
||||
"token_type": "bearer",
|
||||
"scope": "read",
|
||||
"refresh_token": "123456"
|
||||
}
|
||||
客户端模式(Client Credentials)
|
||||
客户端以自己的名义使用客户端ID和密钥向授权服务器请求授权,最简单的授权模式。
|
||||
|
||||
请求链接格式:
|
||||
|
||||
http://localhost:8080/oauth/token?client_id=123456&client_secret=123456&grant_type=client_credentials
|
||||
返回的JSON格式:
|
||||
|
||||
{
|
||||
"access_token": "123456",
|
||||
"token_type": "bearer",
|
||||
"scope": "read",
|
||||
"refresh_token": "123456"
|
||||
}
|
@ -1,6 +1,15 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, CheckCircle2, Code2, Lock, Users } from "lucide-react";
|
||||
import {
|
||||
ArrowDownToLine,
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
Code2,
|
||||
Lock,
|
||||
Send,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@ -9,15 +18,10 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { CodeBlock } from "@/components/ui/code-block";
|
||||
import { Container } from "@/components/ui/container";
|
||||
import { Section } from "@/components/ui/section";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
TypographyH1,
|
||||
TypographyH2,
|
||||
TypographyLead,
|
||||
TypographyMuted,
|
||||
} from "@/components/ui/typography";
|
||||
import { Header } from "@/components/layout/header";
|
||||
|
||||
export default function HomePage() {
|
||||
@ -75,11 +79,11 @@ export default function HomePage() {
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<span>完整的开发文档</span>
|
||||
<span>完整的接入文档</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<span>示例代码和SDK</span>
|
||||
<span>示例代码</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
@ -105,7 +109,7 @@ export default function HomePage() {
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<span>防CSRF攻击</span>
|
||||
<span>Serverless部署 高SLA</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
@ -116,22 +120,22 @@ export default function HomePage() {
|
||||
<Users className="h-10 w-10 text-primary" />
|
||||
<CardTitle className="text-xl">功能丰富</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
提供完整的用户信息和权限管理功能
|
||||
提供完整的应用管理, 仪表盘, API用户信息和权限管理功能
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-4 text-sm">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<span>用户基本信息</span>
|
||||
<span>API 返回信息完善</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<span>用户组权限</span>
|
||||
<span>仪表盘和管理美观界面</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<span>管理员特权</span>
|
||||
<span>限制应用可用者 接入个人应用</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
@ -146,104 +150,223 @@ export default function HomePage() {
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<Card className="overflow-hidden border-2 p-6">
|
||||
<CardHeader className="bg-white dark:bg-gray-900">
|
||||
<CardTitle className="text-2xl">快速开始</CardTitle>
|
||||
<CardTitle className="text-2xl">
|
||||
接入应用 (授权码模式)
|
||||
</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
按照以下步骤,快速接入 Q58 Connect
|
||||
以下示例使用授权码(Authorization
|
||||
Code)模式,这是最安全和最完整的OAuth 2.0授权模式,
|
||||
适用于有后端服务器的应用。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="bg-white p-0 dark:bg-gray-900">
|
||||
<Tabs defaultValue="auth" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="auth">发起授权</TabsTrigger>
|
||||
<TabsTrigger value="callback">处理回调</TabsTrigger>
|
||||
<TabsTrigger value="userinfo">获取用户</TabsTrigger>
|
||||
<TabsTrigger value="auth">
|
||||
<span className="flex h-5 w-6 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
1
|
||||
</span>
|
||||
发起授权
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="callback">
|
||||
<span className="flex h-5 w-6 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
2
|
||||
</span>
|
||||
处理回调
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="userinfo">
|
||||
<span className="flex h-5 w-6 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
3
|
||||
</span>
|
||||
获取用户
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="auth" className="mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>1. 发起授权请求</CardTitle>
|
||||
<CardDescription>
|
||||
将用户重定向到授权页面
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="overflow-x-auto rounded-lg bg-gray-100 p-4 dark:bg-gray-800">
|
||||
<code className="text-sm">
|
||||
{`const authUrl = 'https://connect.q58.club/oauth/authorize?' +
|
||||
<div className="relative mt-6">
|
||||
{/* <div className="absolute left-[3rem] top-0 h-full w-px bg-border" /> */}
|
||||
<TabsContent value="auth" className="mt-4 space-y-4">
|
||||
<Card className="border-2 shadow-sm transition-all hover:border-primary/50">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-white">
|
||||
1
|
||||
</span>
|
||||
<div>
|
||||
<CardTitle>发起OAuth授权请求</CardTitle>
|
||||
<CardDescription>
|
||||
构建授权URL并重定向用户到授权页面
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Alert className="border-l-4 border-l-primary">
|
||||
<Code2 className="h-4 w-4" />
|
||||
<AlertTitle>请求地址</AlertTitle>
|
||||
<AlertDescription className="font-mono">
|
||||
GET https://connect.q58.club/oauth/authorize
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div>
|
||||
<h4 className="mb-2 font-medium">请求参数</h4>
|
||||
<CodeBlock>
|
||||
{`const authUrl = 'https://connect.q58.club/oauth/authorize?' +
|
||||
new URLSearchParams({
|
||||
response_type: 'code', // 必填,固定值
|
||||
response_type: 'code', // 必填,固定值为"code"
|
||||
client_id: 'your_client_id', // 必填,您的应用ID
|
||||
redirect_uri: 'https://your-app.com/callback',
|
||||
state: 'random_state', // 建议提供,防CSRF攻击
|
||||
scope: 'read_profile' // 可选,默认read_profile
|
||||
redirect_uri: 'https://your-app.com/callback', // 必填,回调地址
|
||||
scope: 'read_profile' // 可选,权限范围,默认read_profile
|
||||
});
|
||||
|
||||
window.location.href = authUrl;`}
|
||||
</code>
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="callback" className="mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>2. 处理授权回调</CardTitle>
|
||||
<CardDescription>
|
||||
在回调地址处理授权结果
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="overflow-x-auto rounded-lg bg-gray-100 p-4 dark:bg-gray-800">
|
||||
<code className="text-sm">
|
||||
{`// 获取访问令牌
|
||||
const response = await fetch('https://connect.q58.club/api/oauth/access_token', {
|
||||
</CodeBlock>
|
||||
</div>
|
||||
|
||||
<Alert className="border-green-500">
|
||||
<ArrowDownToLine className="h-4 w-4" />
|
||||
<AlertTitle>授权响应</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p className="mb-2">
|
||||
用户授权后,将重定向到您的回调地址:
|
||||
</p>
|
||||
<pre className="mt-2 overflow-x-auto rounded bg-gray-100 p-4 dark:bg-gray-800">
|
||||
<code className="text-sm">
|
||||
https://your-app.com/callback?code=ac_xxxxxx...
|
||||
</code>
|
||||
</pre>
|
||||
<div className="mt-4 space-y-2">
|
||||
<p className="font-medium">授权码说明:</p>
|
||||
<ul className="list-inside list-disc space-y-1 text-sm text-muted-foreground">
|
||||
<li>格式:ac_前缀 + 40位随机字符</li>
|
||||
<li>有效期:10分钟</li>
|
||||
<li>使用限制:仅可使用一次</li>
|
||||
</ul>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="callback" className="mt-4 space-y-4">
|
||||
<Card className="border-2 shadow-sm transition-all hover:border-primary/50">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-white">
|
||||
2
|
||||
</span>
|
||||
<div>
|
||||
<CardTitle>使用授权码交换访问令牌</CardTitle>
|
||||
<CardDescription>
|
||||
在服务器端使用授权码换取访问令牌
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Alert className="border-l-4 border-l-primary">
|
||||
<Code2 className="h-4 w-4" />
|
||||
<AlertTitle>请求地址</AlertTitle>
|
||||
<AlertDescription className="font-mono">
|
||||
POST
|
||||
https://connect.q58.club/api/oauth/access_token
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div>
|
||||
<h4 className="mb-2 font-medium">请求示例</h4>
|
||||
<CodeBlock>
|
||||
{`const response = await fetch('https://connect.q58.club/api/oauth/access_token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'Basic ' + btoa(client_id + ':' + client_secret)
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
code: '授权码', // 回调参数中的code
|
||||
redirect_uri: 'https://your-app.com/callback'
|
||||
code: 'ac_xxxxx', // 必填,授权码
|
||||
redirect_uri: 'https://your-app.com/callback' // 必填,与请求授权码时相同
|
||||
})
|
||||
});
|
||||
});`}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
|
||||
const { access_token, expires_in } = await response.json();`}
|
||||
</code>
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="userinfo" className="mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>3. 获取用户信息</CardTitle>
|
||||
<CardDescription>
|
||||
使用访问令牌获取用户数据
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="overflow-x-auto rounded-lg bg-gray-100 p-4 dark:bg-gray-800">
|
||||
<code className="text-sm">
|
||||
{`const userInfo = await fetch('https://connect.q58.club/api/oauth/user', {
|
||||
headers: {
|
||||
'Authorization': \`Bearer \${access_token}\`
|
||||
}
|
||||
}).then(res => res.json());
|
||||
|
||||
// 返回数据示例:
|
||||
{
|
||||
"id": "user_xxx",
|
||||
"email": "user@example.com",
|
||||
"username": "username",
|
||||
"name": "用户昵称",
|
||||
"avatar_url": "https://...",
|
||||
"groups": ["group1", "group2"]
|
||||
<Alert className="border-green-500">
|
||||
<ArrowDownToLine className="h-4 w-4" />
|
||||
<AlertTitle>响应数据</AlertTitle>
|
||||
<AlertDescription>
|
||||
<pre className="mt-2 overflow-x-auto rounded bg-gray-100 p-4 dark:bg-gray-800">
|
||||
<code className="text-sm">
|
||||
{`{
|
||||
"access_token": "at_xxxxxxxx", // 访问令牌
|
||||
"token_type": "bearer", // 令牌类型,固定为bearer
|
||||
"expires_in": 604800 // 令牌有效期,单位秒(7天)
|
||||
}`}
|
||||
</code>
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</code>
|
||||
</pre>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="userinfo" className="mt-4 space-y-4">
|
||||
<Card className="border-2 shadow-sm transition-all hover:border-primary/50">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-white">
|
||||
3
|
||||
</span>
|
||||
<div>
|
||||
<CardTitle>获取用户信息</CardTitle>
|
||||
<CardDescription>
|
||||
使用访问令牌获取已授权用户的详细信息
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Alert className="border-l-4 border-l-primary">
|
||||
<Code2 className="h-4 w-4" />
|
||||
<AlertTitle>请求地址</AlertTitle>
|
||||
<AlertDescription className="font-mono">
|
||||
GET https://connect.q58.club/api/oauth/user
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div>
|
||||
<h4 className="mb-2 font-medium">请求示例</h4>
|
||||
<CodeBlock>
|
||||
{`const userInfo = await fetch('https://connect.q58.club/api/oauth/user', {
|
||||
headers: {
|
||||
'Authorization': \`Bearer \${access_token}\` // 使用获取到的访问令牌
|
||||
}
|
||||
}).then(res => res.json());`}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
|
||||
<Alert className="border-green-500">
|
||||
<ArrowDownToLine className="h-4 w-4" />
|
||||
<AlertTitle>响应数据</AlertTitle>
|
||||
<AlertDescription>
|
||||
<pre className="mt-2 overflow-x-auto rounded bg-gray-100 p-4 dark:bg-gray-800">
|
||||
<code className="text-sm">
|
||||
{`{
|
||||
"id": "user_xxx", // 用户唯一标识
|
||||
"email": "user@example.com",// 邮箱地址
|
||||
"username": "username", // 用户名
|
||||
"name": "用户昵称", // 显示名称
|
||||
"avatar_url": "https://...",// 头像URL
|
||||
"admin": false, // 是否管理员
|
||||
"groups": ["group1"], // 用户组
|
||||
}`}
|
||||
</code>
|
||||
</pre>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import type { ExtendedAccessToken, ExtendedClient } from "@/types";
|
||||
import type { ExtendedClient } from "@/types";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
import { getAuthorizationsByClientId } from "@/lib/dto/authorization";
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import { getClientsByUserId } from "@/lib/dto/client";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
|
59
src/components/ui/alert.tsx
Normal file
59
src/components/ui/alert.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDescription.displayName = "AlertDescription";
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
38
src/components/ui/code-block.tsx
Normal file
38
src/components/ui/code-block.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
|
||||
function CopyButton({ text }: { text: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={copy}
|
||||
className="absolute right-2 top-2 rounded-md p-2 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function CodeBlock({ children }: { children: string }) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<pre className="overflow-x-auto rounded-lg bg-gray-100 p-4 dark:bg-gray-800">
|
||||
<code className="text-sm">{children}</code>
|
||||
</pre>
|
||||
<CopyButton text={children} />
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user