login page

This commit is contained in:
2025-04-01 14:57:22 +08:00
parent b8e6180212
commit 696a434b95
11 changed files with 1104 additions and 76 deletions

View File

@@ -0,0 +1,80 @@
'use client';
import Link from 'next/link';
import { ProtectedRoute, useAuth } from '@/lib/auth';
export default function AppLayoutClient({
children,
}: {
children: React.ReactNode;
}) {
const { signOut, user } = useAuth();
const handleSignOut = async () => {
await signOut();
};
return (
<ProtectedRoute>
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<nav className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-16">
<div className="flex items-center">
<Link href="/" className="text-xl font-bold text-gray-900 dark:text-gray-100">
ShortURL Analytics
</Link>
<div className="hidden md:block ml-10">
<div className="flex items-baseline space-x-4">
<Link
href="/dashboard"
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 px-3 py-2 rounded-md text-sm font-medium"
>
Dashboard
</Link>
<Link
href="/events"
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 px-3 py-2 rounded-md text-sm font-medium"
>
Events
</Link>
<Link
href="/analytics/geo"
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 px-3 py-2 rounded-md text-sm font-medium"
>
Geographic
</Link>
<Link
href="/analytics/devices"
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 px-3 py-2 rounded-md text-sm font-medium"
>
Devices
</Link>
</div>
</div>
</div>
<div className="flex items-center">
{user && (
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-700 dark:text-gray-300">
{user.email}
</span>
<button
onClick={handleSignOut}
className="text-sm text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-md"
>
退
</button>
</div>
)}
</div>
</div>
</div>
</nav>
<main className="py-10">
{children}
</main>
</div>
</ProtectedRoute>
);
}

View File

@@ -1,7 +1,7 @@
import '../globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import Link from 'next/link';
import AppLayoutClient from './AppLayoutClient';
const inter = Inter({ subsets: ['latin'] });
@@ -17,50 +17,9 @@ export default function AppLayout({
}) {
return (
<div className={inter.className}>
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<nav className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-16">
<div className="flex items-center">
<Link href="/" className="text-xl font-bold text-gray-900 dark:text-gray-100">
ShortURL Analytics
</Link>
<div className="hidden md:block ml-10">
<div className="flex items-baseline space-x-4">
<Link
href="/dashboard"
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 px-3 py-2 rounded-md text-sm font-medium"
>
Dashboard
</Link>
<Link
href="/events"
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 px-3 py-2 rounded-md text-sm font-medium"
>
Events
</Link>
<Link
href="/analytics/geo"
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 px-3 py-2 rounded-md text-sm font-medium"
>
Geographic
</Link>
<Link
href="/analytics/devices"
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 px-3 py-2 rounded-md text-sm font-medium"
>
Devices
</Link>
</div>
</div>
</div>
</div>
</div>
</nav>
<main className="py-10">
{children}
</main>
</div>
<AppLayoutClient>
{children}
</AppLayoutClient>
</div>
);
}

View File

@@ -1,8 +1,9 @@
import './globals.css';
import type { Metadata } from 'next';
import { AuthProvider } from '@/lib/auth';
export const metadata: Metadata = {
title: 'Link Management & Analytics',
title: 'ShortURL Analytics',
description: 'Track and analyze shortened links',
};
@@ -14,7 +15,9 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body>
{children}
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);

250
app/login/page.tsx Normal file
View File

@@ -0,0 +1,250 @@
'use client';
import { useState, FormEvent, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { useAuth } from '@/lib/auth';
import supabase from '@/lib/supabase';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [serverInfo, setServerInfo] = useState<string | null>(null);
const router = useRouter();
const searchParams = useSearchParams();
const { signIn, signInWithGoogle, user, autoRegisterTestUser } = useAuth();
// 从URL参数中获取消息
useEffect(() => {
const message = searchParams.get('message');
if (message) {
setMessage(message);
}
// 如果用户已登录跳转到dashboard
if (user) {
router.push('/dashboard');
}
// 检查服务器状态
const checkServerStatus = async () => {
try {
const { data, error } = await supabase.auth.getSession();
if (error) {
console.log('Session check error:', error);
setServerInfo(`服务器连接状态: 错误 (${error.message})`);
} else {
console.log('Session check success:', data);
setServerInfo('服务器连接状态: 正常');
}
} catch (error) {
console.error('Server check error:', error);
setServerInfo('服务器连接状态: 无法连接');
}
};
checkServerStatus();
}, [searchParams, user, router]);
// 处理登录表单提交
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError(null);
setIsLoading(true);
try {
console.log('Attempting to sign in with:', { email, password });
await signIn(email, password);
// 登录成功后会自动跳转到dashboard
} catch (error: unknown) {
console.error('Login error:', error);
// 显示更详细的错误信息
if (error instanceof Error) {
setError(`登录失败: ${error.message}`);
} else {
setError('登录失败,请检查邮箱和密码是否正确');
}
} finally {
setIsLoading(false);
}
};
// 处理Google登录
const handleGoogleSignIn = async () => {
setError(null);
try {
await signInWithGoogle();
// 登录流程会重定向到Google然后回到应用
} catch (error: unknown) {
console.error('Google sign in error:', error);
if (error instanceof Error) {
setError(`Google登录失败: ${error.message}`);
} else {
setError('Google登录失败请稍后再试');
}
}
};
// 使用测试账户
const handleUseTestAccount = async () => {
setError(null);
setIsLoading(true);
try {
// 使用AuthContext中的自动注册测试账户功能
await autoRegisterTestUser();
} catch (error) {
console.error('测试账户处理错误:', error);
setError(error instanceof Error ? error.message : '测试账户创建失败');
} finally {
setIsLoading(false);
}
};
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900">
<div className="w-full max-w-md p-8 space-y-8 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100"></h1>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
访
</p>
{serverInfo && (
<div className="mt-2 text-xs text-gray-500 dark:text-gray-500">
{serverInfo}
</div>
)}
</div>
{/* 消息提示 */}
{message && (
<div className="p-4 mb-4 text-sm text-blue-700 bg-blue-100 dark:bg-blue-900 dark:text-blue-200 rounded-lg">
{message}
</div>
)}
{/* 错误提示 */}
{error && (
<div className="p-4 mb-4 text-sm text-red-700 bg-red-100 dark:bg-red-900 dark:text-red-200 rounded-lg">
{error}
</div>
)}
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 dark:placeholder-gray-500 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="your@email.com"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 dark:placeholder-gray-500 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="********"
/>
</div>
</div>
<div>
<button
type="submit"
disabled={isLoading}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? '登录中...' : '登录'}
</button>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300 dark:border-gray-600"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400"></span>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<button
type="button"
onClick={handleGoogleSignIn}
className="flex justify-center items-center py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<svg
className="h-5 w-5 mr-2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<g transform="matrix(1, 0, 0, 1, 27.009001, -39.238998)">
<path
fill="#4285F4"
d="M -3.264 51.509 C -3.264 50.719 -3.334 49.969 -3.454 49.239 L -14.754 49.239 L -14.754 53.749 L -8.284 53.749 C -8.574 55.229 -9.424 56.479 -10.684 57.329 L -10.684 60.329 L -6.824 60.329 C -4.564 58.239 -3.264 55.159 -3.264 51.509 Z"
/>
<path
fill="#34A853"
d="M -14.754 63.239 C -11.514 63.239 -8.804 62.159 -6.824 60.329 L -10.684 57.329 C -11.764 58.049 -13.134 58.489 -14.754 58.489 C -17.884 58.489 -20.534 56.379 -21.484 53.529 L -25.464 53.529 L -25.464 56.619 C -23.494 60.539 -19.444 63.239 -14.754 63.239 Z"
/>
<path
fill="#FBBC05"
d="M -21.484 53.529 C -21.734 52.809 -21.864 52.039 -21.864 51.239 C -21.864 50.439 -21.724 49.669 -21.484 48.949 L -21.484 45.859 L -25.464 45.859 C -26.284 47.479 -26.754 49.299 -26.754 51.239 C -26.754 53.179 -26.284 54.999 -25.464 56.619 L -21.484 53.529 Z"
/>
<path
fill="#EA4335"
d="M -14.754 43.989 C -12.984 43.989 -11.404 44.599 -10.154 45.789 L -6.734 42.369 C -8.804 40.429 -11.514 39.239 -14.754 39.239 C -19.444 39.239 -23.494 41.939 -25.464 45.859 L -21.484 48.949 C -20.534 46.099 -17.884 43.989 -14.754 43.989 Z"
/>
</g>
</svg>
Google登录
</button>
<button
type="button"
onClick={handleUseTestAccount}
className="flex justify-center items-center py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
使
</button>
</div>
</form>
<div className="mt-6 text-center">
<p className="text-sm text-gray-600 dark:text-gray-400">
{' '}
<Link
href="/register"
className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300"
>
</Link>
</p>
</div>
</div>
</div>
);
}

View File

@@ -45,39 +45,65 @@ export default function HomePage() {
];
return (
<div className="container mx-auto px-4">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 dark:text-gray-100 mb-4">
Welcome to ShortURL Analytics
</h1>
<p className="text-lg text-gray-600 dark:text-gray-400">
Get detailed insights into your link performance and visitor behavior
</p>
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<header className="bg-white dark:bg-gray-800 shadow">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
ShortURL Analytics
</h1>
<div>
<Link
href="/login"
className="inline-block bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
</Link>
<Link
href="/register"
className="ml-4 inline-block bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-4 py-2 rounded-md text-sm font-medium hover:bg-gray-300 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
>
</Link>
</div>
</div>
</div>
</header>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{sections.map((section) => (
<Link
key={section.title}
href={section.href}
className="group block p-6 bg-white dark:bg-gray-800 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200"
>
<div className="flex items-center mb-4">
<div className="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg mr-4">
<div className="text-blue-600 dark:text-blue-300">
{section.icon}
<div className="container mx-auto px-4 py-16">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-gray-900 dark:text-gray-100 mb-4">
Welcome to ShortURL Analytics
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
Get detailed insights into your link performance and visitor behavior
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{sections.map((section) => (
<Link
key={section.title}
href={section.href}
className="group block p-6 bg-white dark:bg-gray-800 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200"
>
<div className="flex items-center mb-4">
<div className="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg mr-4">
<div className="text-blue-600 dark:text-blue-300">
{section.icon}
</div>
</div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200">
{section.title}
</h2>
</div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200">
{section.title}
</h2>
</div>
<p className="text-gray-600 dark:text-gray-400">
{section.description}
</p>
</Link>
))}
<p className="text-gray-600 dark:text-gray-400">
{section.description}
</p>
</Link>
))}
</div>
</div>
</div>
</div>

195
app/register/page.tsx Normal file
View File

@@ -0,0 +1,195 @@
'use client';
import { useState, FormEvent } from 'react';
import Link from 'next/link';
import { useAuth } from '@/lib/auth';
export default function RegisterPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { signUp, signInWithGoogle } = useAuth();
// 处理注册表单提交
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError(null);
// 验证密码
if (password !== confirmPassword) {
setError('两次输入的密码不一致');
return;
}
// 密码强度验证
if (password.length < 6) {
setError('密码长度至少为6个字符');
return;
}
setIsLoading(true);
try {
await signUp(email, password);
// 注册成功后会跳转到登录页面,提示用户验证邮箱
} catch (error) {
console.error('Registration error:', error);
setError('注册失败,请稍后再试或使用其他邮箱');
} finally {
setIsLoading(false);
}
};
// 处理Google注册/登录
const handleGoogleSignIn = async () => {
setError(null);
try {
await signInWithGoogle();
// 登录流程会重定向到Google然后回到应用
} catch (error) {
console.error('Google sign in error:', error);
setError('Google登录失败请稍后再试');
}
};
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900">
<div className="w-full max-w-md p-8 space-y-8 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100"></h1>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
访
</p>
</div>
{/* 错误提示 */}
{error && (
<div className="p-4 mb-4 text-sm text-red-700 bg-red-100 dark:bg-red-900 dark:text-red-200 rounded-lg">
{error}
</div>
)}
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 dark:placeholder-gray-500 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="your@email.com"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
</label>
<input
id="password"
name="password"
type="password"
autoComplete="new-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 dark:placeholder-gray-500 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="********"
/>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
autoComplete="new-password"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 dark:placeholder-gray-500 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="********"
/>
</div>
</div>
<div>
<button
type="submit"
disabled={isLoading}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? '注册中...' : '注册'}
</button>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300 dark:border-gray-600"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400"></span>
</div>
</div>
<div>
<button
type="button"
onClick={handleGoogleSignIn}
className="w-full flex justify-center items-center py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<svg
className="h-5 w-5 mr-2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<g transform="matrix(1, 0, 0, 1, 27.009001, -39.238998)">
<path
fill="#4285F4"
d="M -3.264 51.509 C -3.264 50.719 -3.334 49.969 -3.454 49.239 L -14.754 49.239 L -14.754 53.749 L -8.284 53.749 C -8.574 55.229 -9.424 56.479 -10.684 57.329 L -10.684 60.329 L -6.824 60.329 C -4.564 58.239 -3.264 55.159 -3.264 51.509 Z"
/>
<path
fill="#34A853"
d="M -14.754 63.239 C -11.514 63.239 -8.804 62.159 -6.824 60.329 L -10.684 57.329 C -11.764 58.049 -13.134 58.489 -14.754 58.489 C -17.884 58.489 -20.534 56.379 -21.484 53.529 L -25.464 53.529 L -25.464 56.619 C -23.494 60.539 -19.444 63.239 -14.754 63.239 Z"
/>
<path
fill="#FBBC05"
d="M -21.484 53.529 C -21.734 52.809 -21.864 52.039 -21.864 51.239 C -21.864 50.439 -21.724 49.669 -21.484 48.949 L -21.484 45.859 L -25.464 45.859 C -26.284 47.479 -26.754 49.299 -26.754 51.239 C -26.754 53.179 -26.284 54.999 -25.464 56.619 L -21.484 53.529 Z"
/>
<path
fill="#EA4335"
d="M -14.754 43.989 C -12.984 43.989 -11.404 44.599 -10.154 45.789 L -6.734 42.369 C -8.804 40.429 -11.514 39.239 -14.754 39.239 C -19.444 39.239 -23.494 41.939 -25.464 45.859 L -21.484 48.949 C -20.534 46.099 -17.884 43.989 -14.754 43.989 Z"
/>
</g>
</svg>
使Google账号注册
</button>
</div>
</form>
<div className="mt-6 text-center">
<p className="text-sm text-gray-600 dark:text-gray-400">
{' '}
<Link
href="/login"
className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300"
>
</Link>
</p>
</div>
</div>
</div>
);
}