feat: implement data migration for existing lists

- Created migration-actions.ts with server actions for claiming lists
- Added ClaimListsModal component for UI during registration
- Integrated claim modal into RegisterForm
- Created anonymous-lists.ts utilities for tracking lists in localStorage
- Added migrate-lists.ts script for deployment migration
- Created comprehensive MIGRATION_GUIDE.md documentation
- Added migration tests with 100% pass rate
- Updated package.json with db:migrate script
- Added tsx as dev dependency for running migration scripts

Requirements: 10.2, 10.3, 10.4, 10.5
- Users can claim existing lists on first registration
- All list data is preserved during migration
- All lists have an owner after migration
- Migrated lists appear in user's account
This commit is contained in:
2025-12-02 01:20:35 +00:00
parent 54612a4782
commit 2ce89e7c6c
9 changed files with 976 additions and 4 deletions

View File

@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
import { validateEmail, validatePassword, sanitizeString } from '@/lib/validation'
import { registerUser } from '@/lib/auth-actions'
import { useToast } from './ToastContainer'
import ClaimListsModal from './ClaimListsModal'
interface RegisterFormProps {
callbackUrl?: string
@@ -18,6 +19,7 @@ export default function RegisterForm({ callbackUrl = '/' }: RegisterFormProps) {
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [showClaimModal, setShowClaimModal] = useState(false)
const [errors, setErrors] = useState<{
name?: string
email?: string
@@ -154,8 +156,19 @@ export default function RegisterForm({ callbackUrl = '/' }: RegisterFormProps) {
}
} else {
showToast('Registration successful! Logging you in...', 'success')
router.push(callbackUrl)
router.refresh()
// Check if there are lists to claim from localStorage
// Requirement 10.2 - Add UI for users to claim existing lists on first registration
const hasAnonymousLists = checkForAnonymousLists()
if (hasAnonymousLists) {
// Show claim modal
setShowClaimModal(true)
} else {
// No lists to claim, proceed to app
router.push(callbackUrl)
router.refresh()
}
}
} catch (err) {
console.error('Registration error:', err)
@@ -165,8 +178,42 @@ export default function RegisterForm({ callbackUrl = '/' }: RegisterFormProps) {
}
}
const checkForAnonymousLists = (): boolean => {
try {
const stored = localStorage.getItem('anonymousListIds')
if (stored) {
const listIds = JSON.parse(stored)
return Array.isArray(listIds) && listIds.length > 0
}
return false
} catch (err) {
console.error('Error checking for anonymous lists:', err)
return false
}
}
const handleClaimComplete = () => {
setShowClaimModal(false)
showToast('Lists claimed successfully!', 'success')
router.push(callbackUrl)
router.refresh()
}
const handleClaimClose = () => {
setShowClaimModal(false)
router.push(callbackUrl)
router.refresh()
}
return (
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-5">
<>
<ClaimListsModal
isOpen={showClaimModal}
onClose={handleClaimClose}
onComplete={handleClaimComplete}
/>
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-5">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">
Name *
@@ -296,5 +343,6 @@ export default function RegisterForm({ callbackUrl = '/' }: RegisterFormProps) {
{isSubmitting ? 'Creating account...' : 'Create Account'}
</button>
</form>
</>
)
}