feat: Enhance authorization UI and add analytics tracking

This commit is contained in:
wood chen 2025-02-16 22:03:45 +08:00
parent 8be59fe6b6
commit a3fe3ec419
5 changed files with 143 additions and 54 deletions

View File

@ -1,5 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import type { Prisma } from "@prisma/client";
import { getClientsByUserId } from "@/lib/dto/client"; import { getClientsByUserId } from "@/lib/dto/client";
import { getCurrentUser } from "@/lib/session"; import { getCurrentUser } from "@/lib/session";
@ -15,6 +16,8 @@ import {
import { AddClientButton } from "@/components/clients/add-client"; import { AddClientButton } from "@/components/clients/add-client";
import { DeleteClientButton } from "@/components/clients/delete-client"; import { DeleteClientButton } from "@/components/clients/delete-client";
type Client = Awaited<ReturnType<typeof getClientsByUserId>>[number];
// 创建 Prisma 客户端实例 // 创建 Prisma 客户端实例
async function fetchClients(userId: string) { async function fetchClients(userId: string) {
return await getClientsByUserId(userId); return await getClientsByUserId(userId);
@ -50,7 +53,7 @@ export default async function ClientsPage() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{clients.map((client) => ( {clients.map((client: Client) => (
<TableRow key={client.id}> <TableRow key={client.id}>
<TableCell className="font-medium">{client.name}</TableCell> <TableCell className="font-medium">{client.name}</TableCell>
<TableCell className="font-mono text-sm"> <TableCell className="font-mono text-sm">

View File

@ -1,15 +1,18 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import Script from "next/script";
import "@/styles/globals.css"; import "@/styles/globals.css";
import { Toaster } from "@/components/ui/toaster";
import { Providers } from "./providers"; import { Providers } from "./providers";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Q58 Connect", title: "Q58 Connect",
description: "Q58 Connect, 基于Q58论坛的OAuth 2.0认证服务", description: "Q58论坛 OAuth 认证服务",
}; };
export default function RootLayout({ export default function RootLayout({
@ -18,9 +21,20 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="zh-CN" suppressHydrationWarning>
<head>
<Script
defer
src="https://analytics.czl.net/script.js"
data-website-id="78784a68-cb2f-42ae-851b-89c034efd05e"
strategy="afterInteractive"
/>
</head>
<body className={inter.className}> <body className={inter.className}>
<Providers>{children}</Providers> <Providers>
{children}
<Toaster />
</Providers>
</body> </body>
</html> </html>
); );

View File

@ -3,7 +3,6 @@
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { handleAuthorizeAction } from "@/actions/authorizing"; import { handleAuthorizeAction } from "@/actions/authorizing";
import { Client } from "@prisma/client";
import { import {
ChevronsDownUp, ChevronsDownUp,
ChevronsUpDown, ChevronsUpDown,
@ -21,6 +20,8 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Client } from ".prisma/client";
interface Permission { interface Permission {
id: string; id: string;
name: string; name: string;
@ -45,6 +46,7 @@ export function AuthorizationCard({
const [expandedPermission, setExpandedPermission] = useState<string | null>( const [expandedPermission, setExpandedPermission] = useState<string | null>(
null, null,
); );
const [isAuthorizing, setIsAuthorizing] = useState(false);
const router = useRouter(); const router = useRouter();
const togglePermission = (id: string) => { const togglePermission = (id: string) => {
@ -52,6 +54,8 @@ export function AuthorizationCard({
}; };
const authorizingHandler = async () => { const authorizingHandler = async () => {
try {
setIsAuthorizing(true);
const url = await handleAuthorizeAction( const url = await handleAuthorizeAction(
oauthParams, oauthParams,
client.userId, client.userId,
@ -59,68 +63,90 @@ export function AuthorizationCard({
permissions[0].id, permissions[0].id,
); );
router.push(url); router.push(url);
} catch (error) {
setIsAuthorizing(false);
// 这里可以添加错误提示
}
}; };
return ( return (
<Card className="w-full max-w-2xl"> <Card className="w-full max-w-2xl transform transition-all duration-300 hover:shadow-lg">
<CardHeader className="text-center"> <CardHeader className="space-y-4 text-center">
<div className="mb-4 flex items-center justify-center space-x-4"> <div className="flex items-center justify-center space-x-6">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-red-100"> <div className="group relative">
<span className="text-3xl font-bold text-red-500">B</span> <div className="absolute -inset-0.5 rounded-full bg-gradient-to-r from-pink-600 to-purple-600 opacity-50 blur transition duration-300 group-hover:opacity-75"></div>
<div className="relative flex h-20 w-20 items-center justify-center rounded-full bg-white">
<span className="bg-gradient-to-r from-pink-600 to-purple-600 bg-clip-text text-4xl font-bold text-transparent">
{client.name[0].toUpperCase()}
</span>
</div> </div>
<GithubIcon className="h-16 w-16" />
</div> </div>
<CardTitle className="text-2xl font-bold"> {client.name}</CardTitle> </div>
</CardHeader> <CardTitle className="bg-gradient-to-r from-pink-600 to-purple-600 bg-clip-text text-3xl font-bold text-transparent">
<CardContent> {client.name}
<p className="mb-6 text-center"> </CardTitle>
{client.description} <p className="text-sm text-gray-500">
<br /> 访Q58论坛账号
访 Q58论坛
</p> </p>
<div className="space-y-4"> </CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3 rounded-lg bg-gray-50 p-4">
<h3 className="text-lg font-semibold"></h3>
{permissions.map((permission) => ( {permissions.map((permission) => (
<div key={permission.id} className="rounded-lg border p-4">
<div <div
className="flex cursor-pointer items-center justify-between" key={permission.id}
className="rounded-md border bg-white p-4 transition-all duration-200 hover:border-purple-200"
onClick={() => togglePermission(permission.id)} onClick={() => togglePermission(permission.id)}
> >
<div className="flex items-center space-x-3"> <div className="flex cursor-pointer items-center justify-between">
<Checkbox <div className="flex items-center space-x-2">
id={permission.id} <Checkbox id={permission.id} checked disabled />
checked={permission.id === "read_profile"}
disabled={permission.id === "read_profile"}
/>
<label htmlFor={permission.id} className="font-medium"> <label htmlFor={permission.id} className="font-medium">
{permission.name} {permission.name}
</label> </label>
</div> </div>
{expandedPermission === permission.id ? ( {expandedPermission === permission.id ? (
<ChevronsDownUp /> <ChevronsDownUp className="h-4 w-4 text-gray-500" />
) : ( ) : (
<ChevronsUpDown /> <ChevronsUpDown className="h-4 w-4 text-gray-500" />
)} )}
</div> </div>
{expandedPermission === permission.id && ( {expandedPermission === permission.id && (
<p className="mt-2 text-sm text-gray-600"> <div className="mt-2 pl-6 text-sm text-gray-500">
{permission.description} {permission.description}
</p> </div>
)} )}
</div> </div>
))} ))}
</div> </div>
</CardContent> </CardContent>
<CardFooter className="flex flex-col items-center">
<div className="mb-4 flex space-x-4"> <CardFooter className="flex flex-col items-center space-y-4">
<Button variant="outline"></Button> <div className="flex space-x-4">
<Button <Button
className="bg-green-600 text-white hover:bg-green-700" variant="outline"
onClick={authorizingHandler} className="min-w-[100px] transition-all duration-200 hover:bg-gray-100"
disabled={isAuthorizing}
> >
{client.name}
</Button>
<Button
className="min-w-[100px] bg-gradient-to-r from-pink-600 to-purple-600 text-white transition-all duration-200 hover:from-pink-700 hover:to-purple-700"
onClick={authorizingHandler}
disabled={isAuthorizing}
>
{isAuthorizing ? (
<div className="flex items-center">
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent"></div>
...
</div>
) : (
`授权 ${client.name}`
)}
</Button> </Button>
</div> </div>
<p className="mb-4 text-sm text-gray-500"> <p className="text-sm text-gray-500">
{client.redirectUri} {client.redirectUri}
</p> </p>
<div className="flex justify-center space-x-8 text-sm text-gray-500"> <div className="flex justify-center space-x-8 text-sm text-gray-500">

View File

@ -8,6 +8,7 @@ export function Authorizing() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [error, setError] = useState<unknown | null>(null); const [error, setError] = useState<unknown | null>(null);
const [progress, setProgress] = useState(0);
const signInCallback = useCallback(async () => { const signInCallback = useCallback(async () => {
try { try {
@ -19,20 +20,64 @@ export function Authorizing() {
}, []); }, []);
useEffect(() => { useEffect(() => {
// 模拟进度条
const progressInterval = setInterval(() => {
setProgress((prev) => (prev >= 90 ? 90 : prev + 10));
}, 300);
// Delay 3s get sso url go to ... // Delay 3s get sso url go to ...
const timer = setTimeout(signInCallback, 3); const timer = setTimeout(signInCallback, 3000);
return () => { return () => {
clearTimeout(timer); clearTimeout(timer);
clearInterval(progressInterval);
}; };
}, []); }, []);
return ( return (
<> <div className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
{error ? ( {error ? (
<p className="text-center"></p> <div className="space-y-4 text-center">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-red-100">
<svg
className="h-8 w-8 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-900"></h3>
<p className="text-gray-500"></p>
<button
onClick={() => window.location.reload()}
className="mt-4 rounded-md bg-red-600 px-4 py-2 text-white transition-colors hover:bg-red-700"
>
</button>
</div>
) : ( ) : (
<p className="text-center"> ...</p> <div className="space-y-4 text-center">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-blue-100">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-600 border-t-transparent"></div>
</div>
<h3 className="text-xl font-semibold text-gray-900"></h3>
<p className="text-gray-500">...</p>
<div className="h-2.5 w-full rounded-full bg-gray-200">
<div
className="h-2.5 rounded-full bg-blue-600 transition-all duration-300 ease-out"
style={{ width: `${progress}%` }}
></div>
</div>
<p className="text-sm text-gray-400"></p>
</div>
)} )}
</> </div>
); );
} }

View File

@ -2,7 +2,6 @@
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Client } from "@prisma/client";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -16,6 +15,8 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import type { Client } from ".prisma/client";
interface EditClientFormProps { interface EditClientFormProps {
client: Client; client: Client;
} }