better mobile layout

This commit is contained in:
songtianlun 2025-08-03 09:52:59 +08:00
parent 8b5d5ce5eb
commit f0d9797cac
4 changed files with 129 additions and 91 deletions

View File

@ -1,10 +1,10 @@
import { NextRequest, NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma' import { prisma } from '@/lib/prisma'
import { createServerSupabaseClient } from '@/lib/supabase-server' import { createServerSupabaseClient } from '@/lib/supabase-server'
import { addSystemGiftCredit } from '@/lib/credits' import { addSystemGiftCredit } from '@/lib/credits'
// POST /api/users/sync - 同步Supabase用户到Prisma数据库 // POST /api/users/sync - 同步Supabase用户到Prisma数据库
export async function POST(_request: NextRequest) { export async function POST() {
try { try {
const supabase = await createServerSupabaseClient() const supabase = await createServerSupabaseClient()
const { data: { user: supabaseUser }, error: authError } = await supabase.auth.getUser() const { data: { user: supabaseUser }, error: authError } = await supabase.auth.getUser()
@ -68,7 +68,7 @@ export async function POST(_request: NextRequest) {
} }
// GET /api/users/sync - 获取当前用户信息 // GET /api/users/sync - 获取当前用户信息
export async function GET(_request: NextRequest) { export async function GET() {
try { try {
const supabase = await createServerSupabaseClient() const supabase = await createServerSupabaseClient()
const { data: { user: supabaseUser }, error: authError } = await supabase.auth.getUser() const { data: { user: supabaseUser }, error: authError } = await supabase.auth.getUser()

View File

@ -121,29 +121,31 @@ export default function NewPromptPage() {
<div className="border-b"> <div className="border-b">
<div className="max-w-4xl mx-auto px-4 py-4"> <div className="max-w-4xl mx-auto px-4 py-4">
<div className="flex items-center justify-between"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center space-x-4"> <div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => router.back()} onClick={() => router.back()}
className="flex items-center space-x-2" className="flex items-center space-x-2 self-start"
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
<span>{t('backToList')}</span> <span className="hidden sm:inline">{t('backToList')}</span>
<span className="sm:hidden">Back</span>
</Button> </Button>
<div> <div>
<h1 className="text-2xl font-bold text-foreground">{t('newPrompt')}</h1> <h1 className="text-xl sm:text-2xl font-bold text-foreground">{t('newPrompt')}</h1>
<p className="text-sm text-muted-foreground">Create a new AI prompt</p> <p className="text-sm text-muted-foreground">Create a new AI prompt</p>
</div> </div>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center gap-2 order-first sm:order-last">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => router.back()} onClick={() => router.back()}
disabled={isSaving} disabled={isSaving}
className="flex-1 sm:flex-none h-10"
> >
{tCommon('cancel')} {tCommon('cancel')}
</Button> </Button>
@ -152,7 +154,7 @@ export default function NewPromptPage() {
size="sm" size="sm"
onClick={handleSave} onClick={handleSave}
disabled={isSaving || !promptName.trim()} disabled={isSaving || !promptName.trim()}
className="flex items-center space-x-2" className="flex items-center space-x-2 flex-1 sm:flex-none h-10"
> >
{isSaving ? ( {isSaving ? (
<LoadingSpinner size="sm" className="mr-2" /> <LoadingSpinner size="sm" className="mr-2" />
@ -166,11 +168,11 @@ export default function NewPromptPage() {
</div> </div>
</div> </div>
<div className="max-w-4xl mx-auto px-4 py-8"> <div className="max-w-4xl mx-auto px-4 py-6 sm:py-8">
<div className="space-y-6"> <div className="space-y-4 sm:space-y-6">
{/* Basic Information */} {/* Basic Information */}
<div className="bg-card rounded-lg border border-border p-6"> <div className="bg-card rounded-lg border border-border p-4 sm:p-6">
<h2 className="text-lg font-semibold text-foreground mb-4">Basic Information</h2> <h2 className="text-base sm:text-lg font-semibold text-foreground mb-4">Basic Information</h2>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
@ -199,25 +201,27 @@ export default function NewPromptPage() {
</div> </div>
{/* Tags */} {/* Tags */}
<div className="bg-card rounded-lg border border-border p-6"> <div className="bg-card rounded-lg border border-border p-4 sm:p-6">
<h2 className="text-lg font-semibold text-foreground mb-4">{t('tags')}</h2> <h2 className="text-base sm:text-lg font-semibold text-foreground mb-4">{t('tags')}</h2>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex gap-2"> <div className="flex flex-col sm:flex-row gap-2">
<Input <Input
value={newTag} value={newTag}
onChange={(e) => setNewTag(e.target.value)} onChange={(e) => setNewTag(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder={t('addTag')} placeholder={t('addTag')}
className="flex-1" className="flex-1 h-10"
/> />
<Button <Button
type="button" type="button"
onClick={handleAddTag} onClick={handleAddTag}
disabled={!newTag.trim() || tags.includes(newTag.trim())} disabled={!newTag.trim() || tags.includes(newTag.trim())}
className="h-10 sm:w-auto w-full"
> >
<Tag className="h-4 w-4 mr-2" /> <Tag className="h-4 w-4 mr-2" />
{t('addTag')} <span className="hidden sm:inline">{t('addTag')}</span>
<span className="sm:hidden">Add</span>
</Button> </Button>
</div> </div>
@ -245,7 +249,7 @@ export default function NewPromptPage() {
{availableTags.length > 0 && ( {availableTags.length > 0 && (
<div className="mt-4"> <div className="mt-4">
<p className="text-sm text-muted-foreground mb-2">Quick add from existing tags:</p> <p className="text-sm text-muted-foreground mb-2">Quick add from existing tags:</p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-1.5 sm:gap-2">
{availableTags {availableTags
.filter(tag => !tags.includes(tag)) .filter(tag => !tags.includes(tag))
.slice(0, 8) .slice(0, 8)
@ -266,8 +270,8 @@ export default function NewPromptPage() {
</div> </div>
{/* Prompt Content */} {/* Prompt Content */}
<div className="bg-card rounded-lg border border-border p-6"> <div className="bg-card rounded-lg border border-border p-4 sm:p-6">
<h2 className="text-lg font-semibold text-foreground mb-4">{t('promptContent')}</h2> <h2 className="text-base sm:text-lg font-semibold text-foreground mb-4">{t('promptContent')}</h2>
<div> <div>
<Label htmlFor="promptContent">Content</Label> <Label htmlFor="promptContent">Content</Label>
@ -276,7 +280,7 @@ export default function NewPromptPage() {
value={promptContent} value={promptContent}
onChange={(e) => setPromptContent(e.target.value)} onChange={(e) => setPromptContent(e.target.value)}
placeholder="Enter your prompt here..." placeholder="Enter your prompt here..."
className="mt-1 min-h-[300px] font-mono text-sm" className="mt-1 min-h-[200px] sm:min-h-[300px] font-mono text-sm"
/> />
<p className="text-xs text-muted-foreground mt-2"> <p className="text-xs text-muted-foreground mt-2">
Write your prompt instructions here. You can use variables like {'{variable}'} for dynamic content. Write your prompt instructions here. You can use variables like {'{variable}'} for dynamic content.

View File

@ -292,21 +292,22 @@ export default function StudioPage() {
<div className="max-w-7xl mx-auto px-4 py-8"> <div className="max-w-7xl mx-auto px-4 py-8">
{/* Header */} {/* Header */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center justify-between mb-4"> <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-4">
<div> <div className="flex-1">
<h1 className="text-3xl font-bold text-foreground">{t('title')}</h1> <h1 className="text-2xl sm:text-3xl font-bold text-foreground">{t('title')}</h1>
<p className="text-muted-foreground mt-1">{t('myPrompts')}</p> <p className="text-muted-foreground mt-1">{t('myPrompts')}</p>
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:gap-3">
<Button <Button
variant="outline" variant="outline"
onClick={() => setIsBulkAddModalOpen(true)} onClick={() => setIsBulkAddModalOpen(true)}
className="flex items-center space-x-2" className="flex items-center justify-center space-x-2 h-10"
> >
<FolderPlus className="h-4 w-4" /> <FolderPlus className="h-4 w-4" />
<span>Bulk Add</span> <span className="hidden sm:inline">Bulk Add</span>
<span className="sm:hidden">Bulk</span>
</Button> </Button>
<Button onClick={handleCreatePrompt} className="flex items-center space-x-2"> <Button onClick={handleCreatePrompt} className="flex items-center justify-center space-x-2 h-10">
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
<span>{t('createPrompt')}</span> <span>{t('createPrompt')}</span>
</Button> </Button>
@ -314,29 +315,27 @@ export default function StudioPage() {
</div> </div>
{/* Search and Filters */} {/* Search and Filters */}
<div className="bg-muted/30 rounded-xl p-4 mb-6"> <div className="bg-muted/30 rounded-xl p-3 sm:p-4 mb-6">
<div className="flex flex-col lg:flex-row gap-4"> <div className="space-y-3">
{/* Search */} {/* Search */}
<div className="flex-1"> <div className="relative">
<div className="relative"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Input
<Input placeholder={t('searchPrompts')}
placeholder={t('searchPrompts')} value={searchQuery}
value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)}
onChange={(e) => setSearchQuery(e.target.value)} className="pl-9 bg-background border-border/50 focus:border-primary transition-colors h-10"
className="pl-9 bg-background border-border/50 focus:border-primary transition-colors" />
/>
</div>
</div> </div>
{/* Filters Row */} {/* Filters Row */}
<div className="flex flex-col sm:flex-row gap-3"> <div className="flex flex-wrap gap-2">
{/* Tag Filter */} {/* Tag Filter */}
<div className="relative"> <div className="relative flex-1 min-w-[120px]">
<select <select
value={selectedTag} value={selectedTag}
onChange={(e) => setSelectedTag(e.target.value)} onChange={(e) => setSelectedTag(e.target.value)}
className="appearance-none bg-background border border-border/50 rounded-lg px-3 py-2 pr-8 min-w-[130px] focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-colors text-sm" className="appearance-none bg-background border border-border/50 rounded-lg px-3 py-2 pr-8 w-full focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-colors text-sm h-10"
> >
<option value="">{t('allTags')}</option> <option value="">{t('allTags')}</option>
{allTags.map(tag => ( {allTags.map(tag => (
@ -347,7 +346,7 @@ export default function StudioPage() {
</div> </div>
{/* Sort */} {/* Sort */}
<div className="relative"> <div className="relative flex-1 min-w-[140px]">
<select <select
value={`${sortField}-${sortOrder}`} value={`${sortField}-${sortOrder}`}
onChange={(e) => { onChange={(e) => {
@ -355,27 +354,25 @@ export default function StudioPage() {
setSortField(field as SortField) setSortField(field as SortField)
setSortOrder(order as SortOrder) setSortOrder(order as SortOrder)
}} }}
className="appearance-none bg-background border border-border/50 rounded-lg px-3 py-2 pr-8 min-w-[150px] focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-colors text-sm" className="appearance-none bg-background border border-border/50 rounded-lg px-3 py-2 pr-8 w-full focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-colors text-sm h-10"
> >
<option value="updatedAt-desc">{t('sortByUpdated')} ({t('descending')})</option> <option value="updatedAt-desc">Latest</option>
<option value="updatedAt-asc">{t('sortByUpdated')} ({t('ascending')})</option> <option value="updatedAt-asc">Oldest</option>
<option value="createdAt-desc">{t('sortByDate')} ({t('descending')})</option> <option value="name-asc">A-Z</option>
<option value="createdAt-asc">{t('sortByDate')} ({t('ascending')})</option> <option value="name-desc">Z-A</option>
<option value="name-asc">{t('sortByName')} ({t('ascending')})</option> <option value="createdAt-desc">Created Latest</option>
<option value="name-desc">{t('sortByName')} ({t('descending')})</option> <option value="createdAt-asc">Created Oldest</option>
<option value="lastUsed-desc">{t('lastUsed')} ({t('descending')})</option>
<option value="lastUsed-asc">{t('lastUsed')} ({t('ascending')})</option>
</select> </select>
<ChevronDown className="absolute right-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" /> <ChevronDown className="absolute right-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
</div> </div>
{/* View Mode Toggle */} {/* View Mode Toggle */}
<div className="flex items-center bg-background border border-border/50 rounded-lg overflow-hidden"> <div className="flex items-center bg-background border border-border/50 rounded-lg overflow-hidden h-10">
<Button <Button
variant={viewMode === 'grid' ? 'default' : 'ghost'} variant={viewMode === 'grid' ? 'default' : 'ghost'}
size="sm" size="sm"
onClick={() => setViewMode('grid')} onClick={() => setViewMode('grid')}
className="rounded-none border-r border-border/20 h-9" className="rounded-none border-r border-border/20 h-full px-3"
title="Grid View" title="Grid View"
> >
<Grid className="h-4 w-4" /> <Grid className="h-4 w-4" />
@ -384,7 +381,7 @@ export default function StudioPage() {
variant={viewMode === 'list' ? 'default' : 'ghost'} variant={viewMode === 'list' ? 'default' : 'ghost'}
size="sm" size="sm"
onClick={() => setViewMode('list')} onClick={() => setViewMode('list')}
className="rounded-none h-9" className="rounded-none h-full px-3"
title="List View" title="List View"
> >
<List className="h-4 w-4" /> <List className="h-4 w-4" />
@ -394,11 +391,11 @@ export default function StudioPage() {
</div> </div>
{/* Quick Stats */} {/* Quick Stats */}
<div className="flex items-center justify-between mt-4 pt-3 border-t border-border/30"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mt-3 pt-3 border-t border-border/30">
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{prompts.length > 0 ? ( {prompts.length > 0 ? (
<span> <span>
Showing <span className="font-medium text-foreground">{prompts.length}</span> of <span className="font-medium text-foreground">{pagination.total}</span> prompts <span className="hidden sm:inline">Showing </span><span className="font-medium text-foreground">{prompts.length}</span> of <span className="font-medium text-foreground">{pagination.total}</span> prompts
</span> </span>
) : ( ) : (
<span>No prompts found</span> <span>No prompts found</span>
@ -409,7 +406,7 @@ export default function StudioPage() {
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setSearchQuery('')} onClick={() => setSearchQuery('')}
className="text-xs text-muted-foreground hover:text-foreground" className="text-xs text-muted-foreground hover:text-foreground self-start sm:self-auto"
> >
Clear search Clear search
</Button> </Button>
@ -441,7 +438,7 @@ export default function StudioPage() {
<> <>
{/* Prompts Grid/List */} {/* Prompts Grid/List */}
{viewMode === 'grid' ? ( {viewMode === 'grid' ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 lg:gap-6 mb-8"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4 lg:gap-6 mb-8">
{currentPrompts.map((prompt) => ( {currentPrompts.map((prompt) => (
<div <div
key={prompt.id} key={prompt.id}
@ -449,14 +446,14 @@ export default function StudioPage() {
onClick={() => handleShowDetails(prompt)} onClick={() => handleShowDetails(prompt)}
> >
{/* Card Header */} {/* Card Header */}
<div className="p-4 pb-3 flex-1"> <div className="p-3 sm:p-4 pb-3 flex-1">
<div className="mb-3"> <div className="mb-3">
<h3 className="font-semibold text-foreground text-sm leading-tight mb-2 group-hover:text-primary transition-colors"> <h3 className="font-semibold text-foreground text-sm sm:text-base leading-tight mb-2 group-hover:text-primary transition-colors">
<span className="line-clamp-2 break-words" title={prompt.name}> <span className="line-clamp-2 break-words" title={prompt.name}>
{prompt.name} {prompt.name}
</span> </span>
</h3> </h3>
<div className="h-10 overflow-hidden"> <div className="h-8 sm:h-10 overflow-hidden">
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-2"> <p className="text-xs text-muted-foreground leading-relaxed line-clamp-2">
{prompt.description || 'No description available'} {prompt.description || 'No description available'}
</p> </p>
@ -490,7 +487,7 @@ export default function StudioPage() {
</div> </div>
{/* Card Footer */} {/* Card Footer */}
<div className="px-4 pb-4"> <div className="px-3 sm:px-4 pb-3 sm:pb-4">
{/* Metadata */} {/* Metadata */}
<div className="flex items-center justify-between text-xs text-muted-foreground mb-3 pt-2 border-t border-border/50"> <div className="flex items-center justify-between text-xs text-muted-foreground mb-3 pt-2 border-t border-border/50">
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
@ -524,7 +521,7 @@ export default function StudioPage() {
</div> </div>
{/* Action Buttons */} {/* Action Buttons */}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-1 sm:space-x-2">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@ -532,10 +529,11 @@ export default function StudioPage() {
e.stopPropagation() e.stopPropagation()
handleDebugPrompt(prompt.id) handleDebugPrompt(prompt.id)
}} }}
className="flex-1 h-8 text-xs font-medium hover:bg-primary hover:text-primary-foreground transition-colors" className="flex-1 h-7 sm:h-8 text-xs font-medium hover:bg-primary hover:text-primary-foreground transition-colors"
> >
<Play className="h-3 w-3 mr-1" /> <Play className="h-3 w-3 mr-1" />
Open Studio <span className="hidden sm:inline">Open Studio</span>
<span className="sm:hidden">Open</span>
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
@ -544,7 +542,7 @@ export default function StudioPage() {
e.stopPropagation() e.stopPropagation()
handleQuickEdit(prompt) handleQuickEdit(prompt)
}} }}
className="h-8 w-8 p-0 hover:bg-muted transition-colors" className="h-7 sm:h-8 w-7 sm:w-8 p-0 hover:bg-muted transition-colors"
title="Quick Edit" title="Quick Edit"
> >
<Edit className="h-3 w-3" /> <Edit className="h-3 w-3" />
@ -697,22 +695,26 @@ export default function StudioPage() {
{/* Pagination */} {/* Pagination */}
{pagination.totalPages > 1 && ( {pagination.totalPages > 1 && (
<div className="flex items-center justify-between"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-0">
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground text-center sm:text-left">
{t('page')} {pagination.page} {t('of')} {pagination.totalPages} {t('total')} {pagination.total} Page {pagination.page} of {pagination.totalPages}
<span className="hidden sm:inline"> Total {pagination.total}</span>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center justify-center space-x-1 sm:space-x-2">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
disabled={pagination.page === 1} disabled={pagination.page === 1}
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))} onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
className="h-8 px-2 sm:px-3"
> >
Previous <span className="hidden sm:inline">Previous</span>
<span className="sm:hidden">Prev</span>
</Button> </Button>
{Array.from({ length: Math.min(5, pagination.totalPages) }, (_, i) => { {/* Show fewer page numbers on mobile */}
{Array.from({ length: Math.min(3, pagination.totalPages) }, (_, i) => {
const page = i + 1 const page = i + 1
return ( return (
<Button <Button
@ -720,6 +722,7 @@ export default function StudioPage() {
variant={pagination.page === page ? 'default' : 'outline'} variant={pagination.page === page ? 'default' : 'outline'}
size="sm" size="sm"
onClick={() => setCurrentPage(page)} onClick={() => setCurrentPage(page)}
className="h-8 w-8 p-0"
> >
{page} {page}
</Button> </Button>
@ -731,8 +734,10 @@ export default function StudioPage() {
size="sm" size="sm"
disabled={pagination.page === pagination.totalPages} disabled={pagination.page === pagination.totalPages}
onClick={() => setCurrentPage(prev => Math.min(pagination.totalPages, prev + 1))} onClick={() => setCurrentPage(prev => Math.min(pagination.totalPages, prev + 1))}
className="h-8 px-2 sm:px-3"
> >
Next <span className="hidden sm:inline">Next</span>
<span className="sm:hidden">Next</span>
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -2,30 +2,51 @@
import { createClient } from '@/lib/supabase' import { createClient } from '@/lib/supabase'
import { User } from '@supabase/supabase-js' import { User } from '@supabase/supabase-js'
import { useEffect, useState } from 'react' import { useEffect, useState, useRef } from 'react'
export function useAuth() { export function useAuth() {
const [user, setUser] = useState<User | null>(null) const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const supabase = createClient() const supabase = createClient()
const syncTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const lastSyncTimeRef = useRef<number>(0)
const lastUserIdRef = useRef<string | null>(null)
useEffect(() => { useEffect(() => {
// 同步用户到Prisma数据库 // 同步用户到Prisma数据库 - 带防抖和缓存
const syncUser = async () => { const syncUser = async (userId: string) => {
try { const now = Date.now()
await fetch('/api/users/sync', { const timeSinceLastSync = now - lastSyncTimeRef.current
method: 'POST',
headers: { 'Content-Type': 'application/json' } // 如果是同一个用户且距离上次同步不到30秒则跳过
}) if (lastUserIdRef.current === userId && timeSinceLastSync < 30000) {
} catch (error) { return
console.error('Failed to sync user:', error)
} }
// 清除之前的定时器
if (syncTimeoutRef.current) {
clearTimeout(syncTimeoutRef.current)
}
// 防抖处理延迟500ms执行避免频繁调用
syncTimeoutRef.current = setTimeout(async () => {
try {
await fetch('/api/users/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
lastSyncTimeRef.current = Date.now()
lastUserIdRef.current = userId
} catch (error) {
console.error('Failed to sync user:', error)
}
}, 500)
} }
const getUser = async () => { const getUser = async () => {
const { data: { user: userData } } = await supabase.auth.getUser() const { data: { user: userData } } = await supabase.auth.getUser()
if (userData) { if (userData) {
await syncUser() await syncUser(userData.id)
} }
setUser(userData) setUser(userData)
setLoading(false) setLoading(false)
@ -35,14 +56,22 @@ export function useAuth() {
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => { const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
const userData = session?.user ?? null const userData = session?.user ?? null
if (userData && (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED')) {
await syncUser() // 只在首次登录时同步不在token刷新时同步
if (userData && event === 'SIGNED_IN') {
await syncUser(userData.id)
} }
setUser(userData) setUser(userData)
setLoading(false) setLoading(false)
}) })
return () => subscription.unsubscribe() return () => {
subscription.unsubscribe()
if (syncTimeoutRef.current) {
clearTimeout(syncTimeoutRef.current)
}
}
}, [supabase.auth]) }, [supabase.auth])
const signOut = async () => { const signOut = async () => {