ux: 實現翻卡模式動態高度適配與版面優化

- 新增動態高度計算系統,解決翻轉時卡片大小變化問題
- 實現正面背面高度自動匹配,確保翻轉體驗一致
- 優化翻卡模式設計:移除CEFR標籤、簡化正面佈局
- 改善背面內容組織:灰底區分、左對齊文字、移除冗餘元素
- 修復背面底部空白問題,提升視覺整潔度
- 添加平滑高度過渡動畫,增強用戶體驗

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-09-19 23:09:45 +08:00
parent 31e3fe9fa8
commit c1e296c860
1 changed files with 109 additions and 83 deletions

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import { useState, useEffect } from 'react' import { useState, useEffect, useRef, useLayoutEffect } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { Navigation } from '@/components/Navigation' import { Navigation } from '@/components/Navigation'
import AudioPlayer from '@/components/AudioPlayer' import AudioPlayer from '@/components/AudioPlayer'
@ -24,6 +24,12 @@ export default function LearnPage() {
const [reportingCard, setReportingCard] = useState<any>(null) const [reportingCard, setReportingCard] = useState<any>(null)
const [showComplete, setShowComplete] = useState(false) const [showComplete, setShowComplete] = useState(false)
const [quizOptions, setQuizOptions] = useState<string[]>(['brought', 'determine', 'achieve', 'consider']) const [quizOptions, setQuizOptions] = useState<string[]>(['brought', 'determine', 'achieve', 'consider'])
const [cardHeight, setCardHeight] = useState<number>(400)
// Refs for measuring card content heights
const cardFrontRef = useRef<HTMLDivElement>(null)
const cardBackRef = useRef<HTMLDivElement>(null)
const cardContainerRef = useRef<HTMLDivElement>(null)
// Mock data with real example images // Mock data with real example images
const cards = [ const cards = [
@ -70,6 +76,37 @@ export default function LearnPage() {
const currentCard = cards[currentCardIndex] const currentCard = cards[currentCardIndex]
// Calculate optimal card height based on content (only when card changes)
const calculateCardHeight = () => {
if (!cardFrontRef.current || !cardBackRef.current) return 400;
// Get the scroll heights to measure actual content
const frontHeight = cardFrontRef.current.scrollHeight;
const backHeight = cardBackRef.current.scrollHeight;
console.log('Heights calculated:', { frontHeight, backHeight }); // Debug log
// Use the maximum height with padding
const maxHeight = Math.max(frontHeight, backHeight);
const paddedHeight = maxHeight + 40; // Add padding for visual spacing
// Ensure minimum height for visual consistency
return Math.max(paddedHeight, 450);
};
// Update card height only when card content changes (not on flip)
useLayoutEffect(() => {
if (mounted && cardFrontRef.current && cardBackRef.current) {
// Wait for DOM to be fully rendered
const timer = setTimeout(() => {
const newHeight = calculateCardHeight();
setCardHeight(newHeight);
}, 50);
return () => clearTimeout(timer);
}
}, [currentCardIndex, mounted]);
// Client-side mounting and quiz options generation // Client-side mounting and quiz options generation
useEffect(() => { useEffect(() => {
setMounted(true) setMounted(true)
@ -102,6 +139,7 @@ export default function LearnPage() {
setShowResult(false) setShowResult(false)
setFillAnswer('') setFillAnswer('')
setShowHint(false) setShowHint(false)
// Height will be recalculated in useLayoutEffect
} else { } else {
setShowComplete(true) setShowComplete(true)
} }
@ -115,6 +153,7 @@ export default function LearnPage() {
setShowResult(false) setShowResult(false)
setFillAnswer('') setFillAnswer('')
setShowHint(false) setShowHint(false)
// Height will be recalculated in useLayoutEffect
} }
} }
@ -305,72 +344,60 @@ export default function LearnPage() {
</button> </button>
</div> </div>
<div className="card-container" onClick={handleFlip}> <div
ref={cardContainerRef}
className="card-container"
onClick={handleFlip}
style={{ height: `${cardHeight}px` }}
>
<div className={`card ${isFlipped ? 'flipped' : ''}`}> <div className={`card ${isFlipped ? 'flipped' : ''}`}>
{/* Front */} {/* Front */}
<div className="card-front"> <div className="card-front">
<div className="bg-white rounded-xl shadow-lg p-8 text-center cursor-pointer hover:shadow-xl transition-shadow"> <div
<div className="mb-4"> ref={cardFrontRef}
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full"> className="bg-white rounded-xl shadow-lg text-center cursor-pointer hover:shadow-xl transition-shadow"
{currentCard.difficulty} >
</span> <h2 className="text-4xl font-bold text-gray-900 mb-6">
</div>
<h2 className="text-4xl font-bold text-gray-900 mb-4">
{currentCard.word} {currentCard.word}
</h2> </h2>
<p className="text-gray-600 mb-4"> <div className="flex items-center justify-center gap-3">
{currentCard.partOfSpeech} {currentCard.pronunciation} <span className="text-lg text-gray-500">
</p> {currentCard.pronunciation}
</span>
<AudioPlayer text={currentCard.word} /> <AudioPlayer text={currentCard.word} />
<p className="text-sm text-gray-500 mt-4"> </div>
</p>
</div> </div>
</div> </div>
{/* Back */} {/* Back */}
<div className="card-back"> <div className="card-back">
<div className="bg-white rounded-xl shadow-lg p-8 cursor-pointer hover:shadow-xl transition-shadow"> <div
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> ref={cardBackRef}
{/* Left Column - Text Content */} className="bg-white rounded-xl shadow-lg cursor-pointer hover:shadow-xl transition-shadow"
<div> >
<div className="mb-4"> {/* Content Sections */}
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full"> <div className="space-y-4">
{currentCard.difficulty} {/* Definition */}
</span> <div className="bg-gray-50 rounded-lg p-4">
</div> <h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<h2 className="text-3xl font-bold text-gray-900 mb-2"> <p className="text-gray-700 text-left">{currentCard.definition}</p>
{currentCard.word}
</h2>
<p className="text-lg text-blue-600 font-semibold mb-4">
{currentCard.translation}
</p>
<p className="text-gray-600 mb-4">
{currentCard.partOfSpeech} {currentCard.pronunciation}
</p>
<div className="mb-4">
<AudioPlayer text={currentCard.word} />
</div> </div>
<div className="mb-4"> {/* Example */}
<h3 className="font-semibold text-gray-900 mb-2"></h3> <div className="bg-gray-50 rounded-lg p-4">
<p className="text-gray-700">{currentCard.definition}</p> <h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<p className="text-gray-700 italic mb-2 text-left">"{currentCard.example}"</p>
<p className="text-gray-600 text-sm text-left">"{currentCard.exampleTranslation}"</p>
</div> </div>
<div className="mb-4"> {/* Synonyms */}
<h3 className="font-semibold text-gray-900 mb-2"></h3> <div className="bg-gray-50 rounded-lg p-4">
<p className="text-gray-700 italic mb-1">"{currentCard.example}"</p> <h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<p className="text-gray-600 text-sm">"{currentCard.exampleTranslation}"</p>
</div>
<div>
<h3 className="font-semibold text-gray-900 mb-2"></h3>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{currentCard.synonyms.map((synonym, index) => ( {currentCard.synonyms.map((synonym, index) => (
<span <span
key={index} key={index}
className="bg-gray-100 text-gray-700 px-2 py-1 rounded text-sm" className="bg-white text-gray-700 px-3 py-1 rounded-full text-sm border border-gray-200"
> >
{synonym} {synonym}
</span> </span>
@ -378,25 +405,6 @@ export default function LearnPage() {
</div> </div>
</div> </div>
</div> </div>
{/* Right Column - Image */}
<div className="flex items-center justify-center">
<div className="relative">
<img
src={currentCard.exampleImage}
alt={`Example for ${currentCard.word}`}
className="rounded-lg shadow-md max-w-full h-auto cursor-pointer hover:shadow-lg transition-shadow"
onClick={(e) => {
e.stopPropagation()
setModalImage(currentCard.exampleImage)
}}
/>
<div className="absolute top-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded">
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -935,7 +943,9 @@ export default function LearnPage() {
<style jsx>{` <style jsx>{`
.card-container { .card-container {
perspective: 1000px; perspective: 1000px;
height: 400px; transition: height 0.3s ease;
overflow: visible;
position: relative;
} }
.card { .card {
@ -943,7 +953,7 @@ export default function LearnPage() {
width: 100%; width: 100%;
height: 100%; height: 100%;
text-align: center; text-align: center;
transition: transform 0.6s; transition: transform 0.6s ease;
transform-style: preserve-3d; transform-style: preserve-3d;
} }
@ -957,18 +967,34 @@ export default function LearnPage() {
height: 100%; height: 100%;
backface-visibility: hidden; backface-visibility: hidden;
display: flex; display: flex;
align-items: center; align-items: stretch;
justify-content: center; justify-content: center;
top: 0;
left: 0;
} }
.card-back { .card-back {
transform: rotateY(180deg); transform: rotateY(180deg);
} }
.card-front > div, .card-back > div { .card-front > div {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow-y: auto; display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 2rem;
box-sizing: border-box;
}
.card-back > div {
width: 100%;
padding: 1.5rem;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: flex-start;
} }
`}</style> `}</style>
</div> </div>