better mobile layout
This commit is contained in:
parent
8b5d5ce5eb
commit
f0d9797cac
@ -1,10 +1,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { createServerSupabaseClient } from '@/lib/supabase-server'
|
||||
import { addSystemGiftCredit } from '@/lib/credits'
|
||||
|
||||
// POST /api/users/sync - 同步Supabase用户到Prisma数据库
|
||||
export async function POST(_request: NextRequest) {
|
||||
export async function POST() {
|
||||
try {
|
||||
const supabase = await createServerSupabaseClient()
|
||||
const { data: { user: supabaseUser }, error: authError } = await supabase.auth.getUser()
|
||||
@ -68,7 +68,7 @@ export async function POST(_request: NextRequest) {
|
||||
}
|
||||
|
||||
// GET /api/users/sync - 获取当前用户信息
|
||||
export async function GET(_request: NextRequest) {
|
||||
export async function GET() {
|
||||
try {
|
||||
const supabase = await createServerSupabaseClient()
|
||||
const { data: { user: supabaseUser }, error: authError } = await supabase.auth.getUser()
|
||||
|
@ -121,29 +121,31 @@ export default function NewPromptPage() {
|
||||
|
||||
<div className="border-b">
|
||||
<div className="max-w-4xl mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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" />
|
||||
<span>{t('backToList')}</span>
|
||||
<span className="hidden sm:inline">{t('backToList')}</span>
|
||||
<span className="sm:hidden">Back</span>
|
||||
</Button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center gap-2 order-first sm:order-last">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.back()}
|
||||
disabled={isSaving}
|
||||
className="flex-1 sm:flex-none h-10"
|
||||
>
|
||||
{tCommon('cancel')}
|
||||
</Button>
|
||||
@ -152,7 +154,7 @@ export default function NewPromptPage() {
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
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 ? (
|
||||
<LoadingSpinner size="sm" className="mr-2" />
|
||||
@ -166,11 +168,11 @@ export default function NewPromptPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
<div className="space-y-6">
|
||||
<div className="max-w-4xl mx-auto px-4 py-6 sm:py-8">
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{/* Basic Information */}
|
||||
<div className="bg-card rounded-lg border border-border p-6">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">Basic Information</h2>
|
||||
<div className="bg-card rounded-lg border border-border p-4 sm:p-6">
|
||||
<h2 className="text-base sm:text-lg font-semibold text-foreground mb-4">Basic Information</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
@ -199,25 +201,27 @@ export default function NewPromptPage() {
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="bg-card rounded-lg border border-border p-6">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">{t('tags')}</h2>
|
||||
<div className="bg-card rounded-lg border border-border p-4 sm:p-6">
|
||||
<h2 className="text-base sm:text-lg font-semibold text-foreground mb-4">{t('tags')}</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Input
|
||||
value={newTag}
|
||||
onChange={(e) => setNewTag(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t('addTag')}
|
||||
className="flex-1"
|
||||
className="flex-1 h-10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleAddTag}
|
||||
disabled={!newTag.trim() || tags.includes(newTag.trim())}
|
||||
className="h-10 sm:w-auto w-full"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -245,7 +249,7 @@ export default function NewPromptPage() {
|
||||
{availableTags.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<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
|
||||
.filter(tag => !tags.includes(tag))
|
||||
.slice(0, 8)
|
||||
@ -266,8 +270,8 @@ export default function NewPromptPage() {
|
||||
</div>
|
||||
|
||||
{/* Prompt Content */}
|
||||
<div className="bg-card rounded-lg border border-border p-6">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">{t('promptContent')}</h2>
|
||||
<div className="bg-card rounded-lg border border-border p-4 sm:p-6">
|
||||
<h2 className="text-base sm:text-lg font-semibold text-foreground mb-4">{t('promptContent')}</h2>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="promptContent">Content</Label>
|
||||
@ -276,7 +280,7 @@ export default function NewPromptPage() {
|
||||
value={promptContent}
|
||||
onChange={(e) => setPromptContent(e.target.value)}
|
||||
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">
|
||||
Write your prompt instructions here. You can use variables like {'{variable}'} for dynamic content.
|
||||
|
@ -292,21 +292,22 @@ export default function StudioPage() {
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">{t('title')}</h1>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-4">
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-foreground">{t('title')}</h1>
|
||||
<p className="text-muted-foreground mt-1">{t('myPrompts')}</p>
|
||||
</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
|
||||
variant="outline"
|
||||
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" />
|
||||
<span>Bulk Add</span>
|
||||
<span className="hidden sm:inline">Bulk Add</span>
|
||||
<span className="sm:hidden">Bulk</span>
|
||||
</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" />
|
||||
<span>{t('createPrompt')}</span>
|
||||
</Button>
|
||||
@ -314,29 +315,27 @@ export default function StudioPage() {
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<div className="bg-muted/30 rounded-xl p-4 mb-6">
|
||||
<div className="flex flex-col lg:flex-row gap-4">
|
||||
<div className="bg-muted/30 rounded-xl p-3 sm:p-4 mb-6">
|
||||
<div className="space-y-3">
|
||||
{/* Search */}
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t('searchPrompts')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 bg-background border-border/50 focus:border-primary transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t('searchPrompts')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 bg-background border-border/50 focus:border-primary transition-colors h-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filters Row */}
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{/* Tag Filter */}
|
||||
<div className="relative">
|
||||
<div className="relative flex-1 min-w-[120px]">
|
||||
<select
|
||||
value={selectedTag}
|
||||
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>
|
||||
{allTags.map(tag => (
|
||||
@ -347,7 +346,7 @@ export default function StudioPage() {
|
||||
</div>
|
||||
|
||||
{/* Sort */}
|
||||
<div className="relative">
|
||||
<div className="relative flex-1 min-w-[140px]">
|
||||
<select
|
||||
value={`${sortField}-${sortOrder}`}
|
||||
onChange={(e) => {
|
||||
@ -355,27 +354,25 @@ export default function StudioPage() {
|
||||
setSortField(field as SortField)
|
||||
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-asc">{t('sortByUpdated')} ({t('ascending')})</option>
|
||||
<option value="createdAt-desc">{t('sortByDate')} ({t('descending')})</option>
|
||||
<option value="createdAt-asc">{t('sortByDate')} ({t('ascending')})</option>
|
||||
<option value="name-asc">{t('sortByName')} ({t('ascending')})</option>
|
||||
<option value="name-desc">{t('sortByName')} ({t('descending')})</option>
|
||||
<option value="lastUsed-desc">{t('lastUsed')} ({t('descending')})</option>
|
||||
<option value="lastUsed-asc">{t('lastUsed')} ({t('ascending')})</option>
|
||||
<option value="updatedAt-desc">Latest</option>
|
||||
<option value="updatedAt-asc">Oldest</option>
|
||||
<option value="name-asc">A-Z</option>
|
||||
<option value="name-desc">Z-A</option>
|
||||
<option value="createdAt-desc">Created Latest</option>
|
||||
<option value="createdAt-asc">Created Oldest</option>
|
||||
</select>
|
||||
<ChevronDown className="absolute right-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
variant={viewMode === 'grid' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
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"
|
||||
>
|
||||
<Grid className="h-4 w-4" />
|
||||
@ -384,7 +381,7 @@ export default function StudioPage() {
|
||||
variant={viewMode === 'list' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('list')}
|
||||
className="rounded-none h-9"
|
||||
className="rounded-none h-full px-3"
|
||||
title="List View"
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
@ -394,11 +391,11 @@ export default function StudioPage() {
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
{prompts.length > 0 ? (
|
||||
<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>No prompts found</span>
|
||||
@ -409,7 +406,7 @@ export default function StudioPage() {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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
|
||||
</Button>
|
||||
@ -441,7 +438,7 @@ export default function StudioPage() {
|
||||
<>
|
||||
{/* Prompts Grid/List */}
|
||||
{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) => (
|
||||
<div
|
||||
key={prompt.id}
|
||||
@ -449,14 +446,14 @@ export default function StudioPage() {
|
||||
onClick={() => handleShowDetails(prompt)}
|
||||
>
|
||||
{/* 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">
|
||||
<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}>
|
||||
{prompt.name}
|
||||
</span>
|
||||
</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">
|
||||
{prompt.description || 'No description available'}
|
||||
</p>
|
||||
@ -490,7 +487,7 @@ export default function StudioPage() {
|
||||
</div>
|
||||
|
||||
{/* Card Footer */}
|
||||
<div className="px-4 pb-4">
|
||||
<div className="px-3 sm:px-4 pb-3 sm:pb-4">
|
||||
{/* 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 space-x-1">
|
||||
@ -524,7 +521,7 @@ export default function StudioPage() {
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-1 sm:space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@ -532,10 +529,11 @@ export default function StudioPage() {
|
||||
e.stopPropagation()
|
||||
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" />
|
||||
Open Studio
|
||||
<span className="hidden sm:inline">Open Studio</span>
|
||||
<span className="sm:hidden">Open</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@ -544,7 +542,7 @@ export default function StudioPage() {
|
||||
e.stopPropagation()
|
||||
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"
|
||||
>
|
||||
<Edit className="h-3 w-3" />
|
||||
@ -697,22 +695,26 @@ export default function StudioPage() {
|
||||
|
||||
{/* Pagination */}
|
||||
{pagination.totalPages > 1 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('page')} {pagination.page} {t('of')} {pagination.totalPages} {t('total')} {pagination.total}
|
||||
<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 text-center sm:text-left">
|
||||
Page {pagination.page} of {pagination.totalPages}
|
||||
<span className="hidden sm:inline"> • Total {pagination.total}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center justify-center space-x-1 sm:space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={pagination.page === 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>
|
||||
|
||||
{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
|
||||
return (
|
||||
<Button
|
||||
@ -720,6 +722,7 @@ export default function StudioPage() {
|
||||
variant={pagination.page === page ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(page)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
{page}
|
||||
</Button>
|
||||
@ -731,8 +734,10 @@ export default function StudioPage() {
|
||||
size="sm"
|
||||
disabled={pagination.page === pagination.totalPages}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,30 +2,51 @@
|
||||
|
||||
import { createClient } from '@/lib/supabase'
|
||||
import { User } from '@supabase/supabase-js'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
|
||||
export function useAuth() {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const supabase = createClient()
|
||||
const syncTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const lastSyncTimeRef = useRef<number>(0)
|
||||
const lastUserIdRef = useRef<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// 同步用户到Prisma数据库
|
||||
const syncUser = async () => {
|
||||
try {
|
||||
await fetch('/api/users/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to sync user:', error)
|
||||
// 同步用户到Prisma数据库 - 带防抖和缓存
|
||||
const syncUser = async (userId: string) => {
|
||||
const now = Date.now()
|
||||
const timeSinceLastSync = now - lastSyncTimeRef.current
|
||||
|
||||
// 如果是同一个用户且距离上次同步不到30秒,则跳过
|
||||
if (lastUserIdRef.current === userId && timeSinceLastSync < 30000) {
|
||||
return
|
||||
}
|
||||
|
||||
// 清除之前的定时器
|
||||
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 { data: { user: userData } } = await supabase.auth.getUser()
|
||||
if (userData) {
|
||||
await syncUser()
|
||||
await syncUser(userData.id)
|
||||
}
|
||||
setUser(userData)
|
||||
setLoading(false)
|
||||
@ -35,14 +56,22 @@ export function useAuth() {
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||
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)
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
return () => {
|
||||
subscription.unsubscribe()
|
||||
if (syncTimeoutRef.current) {
|
||||
clearTimeout(syncTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [supabase.auth])
|
||||
|
||||
const signOut = async () => {
|
||||
|
Loading…
Reference in New Issue
Block a user