新增資料庫架構

This commit is contained in:
2025-07-19 02:12:37 +08:00
parent e3832acfa8
commit 924f03c3d7
45 changed files with 12858 additions and 324 deletions

View File

@@ -0,0 +1,314 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Progress } from "@/components/ui/progress"
import { Database, Upload, CheckCircle, XCircle, AlertTriangle, Loader2, Trash2, RefreshCw } from "lucide-react"
import { MigrationService, testSupabaseConnection } from "@/lib/supabase-service"
interface MigrationDialogProps {
onComplete?: () => void
onSkip?: () => void
}
export default function MigrationDialog({ onComplete, onSkip }: MigrationDialogProps) {
const [step, setStep] = useState<"check" | "migrate" | "complete" | "error">("check")
const [localDataCount, setLocalDataCount] = useState(0)
const [migrationResult, setMigrationResult] = useState<{
success: number
failed: number
errors: string[]
} | null>(null)
const [isConnected, setIsConnected] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [progress, setProgress] = useState(0)
useEffect(() => {
checkLocalData()
checkConnection()
}, [])
const checkLocalData = () => {
try {
const wishes = JSON.parse(localStorage.getItem("wishes") || "[]")
setLocalDataCount(wishes.length)
} catch (error) {
console.error("Error checking local data:", error)
setLocalDataCount(0)
}
}
const checkConnection = async () => {
setIsLoading(true)
try {
const connected = await testSupabaseConnection()
setIsConnected(connected)
} catch (error) {
console.error("Connection check failed:", error)
setIsConnected(false)
} finally {
setIsLoading(false)
}
}
const startMigration = async () => {
if (!isConnected) {
alert("請先確保 Supabase 連接正常")
return
}
setStep("migrate")
setIsLoading(true)
setProgress(0)
try {
// 模擬進度更新
const progressInterval = setInterval(() => {
setProgress((prev) => Math.min(prev + 10, 90))
}, 200)
const result = await MigrationService.migrateWishesFromLocalStorage()
clearInterval(progressInterval)
setProgress(100)
setMigrationResult(result)
if (result.success > 0) {
setStep("complete")
} else {
setStep("error")
}
} catch (error) {
console.error("Migration failed:", error)
setMigrationResult({
success: 0,
failed: localDataCount,
errors: [`遷移過程失敗: ${error}`],
})
setStep("error")
} finally {
setIsLoading(false)
}
}
const clearLocalData = () => {
if (confirm("確定要清除本地數據嗎?此操作無法復原。")) {
MigrationService.clearLocalStorageData()
setLocalDataCount(0)
onComplete?.()
}
}
const skipMigration = () => {
if (confirm("跳過遷移將繼續使用本地存儲。確定要跳過嗎?")) {
onSkip?.()
}
}
if (localDataCount === 0) {
return (
<Card className="bg-slate-800/50 backdrop-blur-sm border border-green-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-3">
<CheckCircle className="w-6 h-6 text-green-400" />
</CardTitle>
<CardDescription className="text-green-200">使 Supabase</CardDescription>
</CardHeader>
<CardContent>
<Button
onClick={onComplete}
className="w-full bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white"
>
使
</Button>
</CardContent>
</Card>
)
}
return (
<Card className="bg-slate-800/50 backdrop-blur-sm border border-blue-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-3">
<Database className="w-6 h-6 text-blue-400" />
Supabase
</CardTitle>
<CardDescription className="text-blue-200">
{localDataCount}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* 連接狀態 */}
<div className="flex items-center justify-between p-3 bg-slate-700/50 rounded-lg">
<div className="flex items-center gap-2">
<div className={`w-3 h-3 rounded-full ${isConnected ? "bg-green-400" : "bg-red-400"}`}></div>
<span className="text-white text-sm">Supabase </span>
</div>
<div className="flex items-center gap-2">
<Badge className={isConnected ? "bg-green-500/20 text-green-200" : "bg-red-500/20 text-red-200"}>
{isConnected ? "已連接" : "未連接"}
</Badge>
<Button
variant="ghost"
size="sm"
onClick={checkConnection}
disabled={isLoading}
className="text-blue-200 hover:text-white"
>
<RefreshCw className={`w-4 h-4 ${isLoading ? "animate-spin" : ""}`} />
</Button>
</div>
</div>
{step === "check" && (
<div className="space-y-4">
<Alert className="border-blue-500/50 bg-blue-900/20">
<AlertTriangle className="h-4 w-4 text-blue-400" />
<AlertDescription className="text-blue-100">
<div className="space-y-2">
<p>
<strong></strong>
</p>
<ul className="text-sm space-y-1 ml-4">
<li> </li>
<li> </li>
<li> </li>
<li> 使</li>
</ul>
</div>
</AlertDescription>
</Alert>
<div className="flex gap-3">
<Button
onClick={startMigration}
disabled={!isConnected || isLoading}
className="flex-1 bg-gradient-to-r from-blue-500 to-cyan-600 hover:from-blue-600 hover:to-cyan-700 text-white"
>
<Upload className="w-4 h-4 mr-2" />
</Button>
<Button
variant="outline"
onClick={skipMigration}
className="border-slate-600 text-slate-300 hover:bg-slate-700 bg-transparent"
>
</Button>
</div>
</div>
)}
{step === "migrate" && (
<div className="space-y-4">
<div className="text-center">
<Loader2 className="w-8 h-8 animate-spin text-blue-400 mx-auto mb-2" />
<p className="text-white">...</p>
</div>
<Progress value={progress} className="w-full" />
<p className="text-sm text-slate-300 text-center"> {localDataCount} </p>
</div>
)}
{step === "complete" && migrationResult && (
<div className="space-y-4">
<div className="text-center">
<CheckCircle className="w-12 h-12 text-green-400 mx-auto mb-3" />
<h3 className="text-xl font-semibold text-white mb-2"></h3>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="text-center p-3 bg-green-900/20 rounded-lg border border-green-600/30">
<div className="text-2xl font-bold text-green-400">{migrationResult.success}</div>
<div className="text-sm text-green-200"></div>
</div>
<div className="text-center p-3 bg-red-900/20 rounded-lg border border-red-600/30">
<div className="text-2xl font-bold text-red-400">{migrationResult.failed}</div>
<div className="text-sm text-red-200"></div>
</div>
</div>
{migrationResult.errors.length > 0 && (
<Alert className="border-yellow-500/50 bg-yellow-900/20">
<AlertTriangle className="h-4 w-4 text-yellow-400" />
<AlertDescription className="text-yellow-100">
<details>
<summary className="cursor-pointer"></summary>
<div className="mt-2 text-xs space-y-1">
{migrationResult.errors.map((error, index) => (
<div key={index}> {error}</div>
))}
</div>
</details>
</AlertDescription>
</Alert>
)}
<div className="flex gap-3">
<Button
onClick={clearLocalData}
className="flex-1 bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white"
>
<Trash2 className="w-4 h-4 mr-2" />
</Button>
<Button
variant="outline"
onClick={onComplete}
className="border-slate-600 text-slate-300 hover:bg-slate-700 bg-transparent"
>
</Button>
</div>
</div>
)}
{step === "error" && migrationResult && (
<div className="space-y-4">
<div className="text-center">
<XCircle className="w-12 h-12 text-red-400 mx-auto mb-3" />
<h3 className="text-xl font-semibold text-white mb-2"></h3>
</div>
<Alert className="border-red-500/50 bg-red-900/20">
<XCircle className="h-4 w-4 text-red-400" />
<AlertDescription className="text-red-100">
<div className="space-y-2">
<p></p>
<div className="text-xs space-y-1 ml-4">
{migrationResult.errors.map((error, index) => (
<div key={index}> {error}</div>
))}
</div>
</div>
</AlertDescription>
</Alert>
<div className="flex gap-3">
<Button
onClick={startMigration}
disabled={!isConnected}
className="flex-1 bg-gradient-to-r from-blue-500 to-cyan-600 hover:from-blue-600 hover:to-cyan-700 text-white"
>
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
<Button
variant="outline"
onClick={skipMigration}
className="border-slate-600 text-slate-300 hover:bg-slate-700 bg-transparent"
>
</Button>
</div>
</div>
)}
</CardContent>
</Card>
)
}