feat: create professional, polished UI with perfect spacing
- Add generous padding and spacing throughout (p-10, p-12) - Constrain content width with max-w-4xl and max-w-5xl - Increase font sizes for better hierarchy (text-3xl, text-lg) - Add gradient backgrounds to cards and elements - Implement smooth transitions and hover states - Create professional progress stepper with labels - Add shadow effects with color-matched glows - Increase button padding and font sizes - Add gradient borders on hover - Implement proper visual breathing room - Create polished, premium aesthetic - Add smooth cubic-bezier transitions - Improve form input styling with focus rings - Create professional card hover effects
This commit is contained in:
@@ -158,22 +158,22 @@ function App() {
|
||||
const allRolesAssigned = roles.length > 0 && roles.every(role => voiceAssignments.has(role.id))
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<div className="container mx-auto py-8 px-4 sm:py-12 sm:px-6 lg:px-8 max-w-5xl">
|
||||
<div className="min-h-screen">
|
||||
<div className="container mx-auto py-12 px-4 sm:px-6 lg:px-8">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl mb-6 shadow-lg">
|
||||
<span className="text-3xl">🎧</span>
|
||||
<div className="text-center mb-16 max-w-3xl mx-auto">
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-3xl mb-8 shadow-xl shadow-indigo-500/30">
|
||||
<span className="text-4xl">🎧</span>
|
||||
</div>
|
||||
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold text-gray-900 mb-4">
|
||||
<h1 className="text-5xl sm:text-6xl md:text-7xl font-black text-gray-900 mb-6 tracking-tight">
|
||||
Text to Audiobook
|
||||
</h1>
|
||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||
<p className="text-xl text-gray-600 leading-relaxed">
|
||||
Transform your stories into immersive audio experiences with AI-powered voices
|
||||
</p>
|
||||
|
||||
{/* Progress Steps */}
|
||||
<div className="mt-10 flex justify-center items-center gap-3">
|
||||
<div className="mt-12 flex justify-center items-center gap-2">
|
||||
{['Input', 'Voices', 'Review', 'Done'].map((step, index) => {
|
||||
const phases = ['input', 'assignment', 'review', 'generated'];
|
||||
const isActive = phase === phases[index];
|
||||
@@ -181,15 +181,20 @@ function App() {
|
||||
|
||||
return (
|
||||
<div key={step} className="flex items-center">
|
||||
<div className={`flex items-center justify-center w-10 h-10 rounded-full font-semibold text-sm transition-all ${
|
||||
isActive ? 'bg-indigo-600 text-white scale-110' :
|
||||
isPast ? 'bg-green-500 text-white' :
|
||||
'bg-gray-200 text-gray-500'
|
||||
}`}>
|
||||
{isPast ? '✓' : index + 1}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`flex items-center justify-center w-12 h-12 rounded-full font-bold text-sm transition-all duration-300 ${
|
||||
isActive ? 'bg-indigo-600 text-white scale-110 shadow-lg shadow-indigo-500/50' :
|
||||
isPast ? 'bg-green-500 text-white' :
|
||||
'bg-gray-200 text-gray-500'
|
||||
}`}>
|
||||
{isPast ? '✓' : index + 1}
|
||||
</div>
|
||||
<span className={`text-xs font-semibold mt-2 ${isActive ? 'text-indigo-600' : 'text-gray-500'}`}>
|
||||
{step}
|
||||
</span>
|
||||
</div>
|
||||
{index < 3 && (
|
||||
<div className={`w-12 h-1 mx-2 rounded ${isPast ? 'bg-green-500' : 'bg-gray-200'}`}></div>
|
||||
<div className={`w-16 h-1 mx-3 rounded-full transition-all duration-300 ${isPast || isActive ? 'bg-indigo-600' : 'bg-gray-200'}`}></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,48 +8,50 @@ export default function AudioPlayerSection({
|
||||
onDownload
|
||||
}: AudioPlayerSectionProps) {
|
||||
return (
|
||||
<div className="card-modern p-8 space-y-6">
|
||||
{/* Success Header */}
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-green-400 to-emerald-500 rounded-2xl mb-4">
|
||||
<span className="text-3xl">🎉</span>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="card-modern p-10 sm:p-12 space-y-10">
|
||||
{/* Success Header */}
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-24 h-24 bg-gradient-to-br from-green-400 to-emerald-500 rounded-3xl mb-6 shadow-xl shadow-green-500/30">
|
||||
<span className="text-5xl">🎉</span>
|
||||
</div>
|
||||
<h2 className="text-4xl font-black text-gray-900 mb-4">
|
||||
Your Audiobook is Ready!
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
Listen below or download to enjoy offline
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Audio Player */}
|
||||
<div className="p-8 bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 rounded-2xl border-2 border-indigo-200">
|
||||
<audio
|
||||
controls
|
||||
src={audioUrl}
|
||||
className="w-full"
|
||||
preload="metadata"
|
||||
>
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
Your Audiobook is Ready!
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
Listen below or download to enjoy offline
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Audio Player */}
|
||||
<div className="p-6 bg-gradient-to-br from-indigo-50 to-purple-50 rounded-xl border border-indigo-100">
|
||||
<audio
|
||||
controls
|
||||
src={audioUrl}
|
||||
className="w-full"
|
||||
preload="metadata"
|
||||
>
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
{/* Download Button */}
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
onClick={onDownload}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<span>⬇️</span>
|
||||
Download Audiobook
|
||||
</button>
|
||||
</div>
|
||||
{/* Download Button */}
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
onClick={onDownload}
|
||||
className="btn-primary flex items-center gap-3"
|
||||
>
|
||||
<span className="text-xl">⬇️</span>
|
||||
Download Audiobook
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Success Message */}
|
||||
<div className="p-4 bg-green-50 border border-green-200 text-green-800 rounded-xl text-center">
|
||||
<p className="font-medium">
|
||||
✅ Your audiobook has been successfully generated!
|
||||
</p>
|
||||
{/* Success Message */}
|
||||
<div className="p-6 bg-green-50 border-2 border-green-200 text-green-900 rounded-2xl text-center">
|
||||
<p className="font-bold text-lg">
|
||||
✅ Your audiobook has been successfully generated!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,83 +22,85 @@ export default function ScriptReviewSection({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card-modern p-8 space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Review Script</h2>
|
||||
<p className="text-gray-600">Review and adjust speaker assignments before generating your audiobook.</p>
|
||||
</div>
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="card-modern p-10 sm:p-12 space-y-10">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-3">Review Script</h2>
|
||||
<p className="text-lg text-gray-600">Review and adjust speaker assignments before generating your audiobook.</p>
|
||||
</div>
|
||||
|
||||
{/* Script Segments */}
|
||||
<div className="space-y-4">
|
||||
{segments.map((segment, index) => (
|
||||
<div
|
||||
key={segment.id}
|
||||
className="p-5 bg-gray-50 rounded-xl border border-gray-200"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3 mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center w-8 h-8 bg-indigo-100 text-indigo-700 rounded-lg font-semibold text-sm">
|
||||
{index + 1}
|
||||
{/* Script Segments */}
|
||||
<div className="space-y-6">
|
||||
{segments.map((segment, index) => (
|
||||
<div
|
||||
key={segment.id}
|
||||
className="p-6 bg-gradient-to-br from-gray-50 to-white rounded-2xl border-2 border-gray-200 hover:border-indigo-300 transition-all duration-200"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-4 mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 text-white rounded-xl font-bold text-sm shadow-lg shadow-indigo-500/30">
|
||||
{index + 1}
|
||||
</div>
|
||||
<span className="text-sm font-bold text-gray-500 uppercase tracking-wide">Segment</span>
|
||||
</div>
|
||||
|
||||
{/* Speaker Dropdown */}
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<label className="text-base font-bold text-gray-700 whitespace-nowrap">
|
||||
Speaker:
|
||||
</label>
|
||||
<select
|
||||
value={segment.speaker}
|
||||
onChange={(e) => handleSpeakerChange(segment.id, e.target.value)}
|
||||
className="flex-1 p-3 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-4 focus:ring-indigo-100 focus:outline-none bg-white font-medium transition-all duration-200"
|
||||
disabled={isGenerating}
|
||||
>
|
||||
{roles.map((role) => (
|
||||
<option key={role.id} value={role.id}>
|
||||
{role.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-gray-500 uppercase">Segment</span>
|
||||
</div>
|
||||
|
||||
{/* Speaker Dropdown */}
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<label className="text-sm font-semibold text-gray-700 whitespace-nowrap">
|
||||
Speaker:
|
||||
</label>
|
||||
<select
|
||||
value={segment.speaker}
|
||||
onChange={(e) => handleSpeakerChange(segment.id, e.target.value)}
|
||||
className="flex-1 p-2 border-2 border-gray-200 rounded-lg focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:outline-none bg-white"
|
||||
disabled={isGenerating}
|
||||
>
|
||||
{roles.map((role) => (
|
||||
<option key={role.id} value={role.id}>
|
||||
{role.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Segment Text */}
|
||||
<div className="p-5 bg-white rounded-xl border-2 border-gray-200 text-gray-800 whitespace-pre-wrap leading-relaxed text-base">
|
||||
{segment.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Segment Text */}
|
||||
<div className="p-4 bg-white rounded-lg border border-gray-200 text-gray-800 whitespace-pre-wrap leading-relaxed">
|
||||
{segment.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-end pt-4">
|
||||
<button
|
||||
onClick={onSkipReview}
|
||||
disabled={isGenerating}
|
||||
className="px-6 py-3 border-2 border-gray-300 text-gray-700 font-semibold rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onGenerate}
|
||||
disabled={isGenerating}
|
||||
className="btn-primary"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
|
||||
Generating...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-2">
|
||||
Generate Audiobook
|
||||
<span>✨</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-end pt-4">
|
||||
<button
|
||||
onClick={onSkipReview}
|
||||
disabled={isGenerating}
|
||||
className="px-8 py-4 border-2 border-gray-300 text-gray-700 font-semibold rounded-xl hover:bg-gray-50 hover:border-gray-400 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onGenerate}
|
||||
disabled={isGenerating}
|
||||
className="btn-primary"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<span className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
|
||||
Generating...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-3">
|
||||
Generate Audiobook
|
||||
<span className="text-lg">✨</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -36,56 +36,58 @@ export default function TextInputSection({
|
||||
const isButtonDisabled = disabled || isLoading || text.trim() === '';
|
||||
|
||||
return (
|
||||
<div className="card-modern p-8">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Enter Your Text</h2>
|
||||
<p className="text-gray-600">Paste or type your story below. Use patterns like "Name:" to mark different speakers.</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 relative">
|
||||
<textarea
|
||||
value={text}
|
||||
onChange={handleTextChange}
|
||||
placeholder="Example:
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="card-modern p-10 sm:p-12">
|
||||
<div className="mb-10">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-3">Enter Your Text</h2>
|
||||
<p className="text-lg text-gray-600">Paste or type your story below. Use patterns like "Name:" to mark different speakers.</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 relative">
|
||||
<textarea
|
||||
value={text}
|
||||
onChange={handleTextChange}
|
||||
placeholder="Example:
|
||||
|
||||
Sarah: Hello! How are you today?
|
||||
John: I'm doing great, thanks for asking!
|
||||
Sarah: That's wonderful to hear."
|
||||
className="w-full h-80 p-4 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:outline-none resize-y text-base leading-relaxed"
|
||||
disabled={disabled || isLoading}
|
||||
/>
|
||||
<div className="absolute bottom-3 right-3 text-xs text-gray-500 bg-white px-2 py-1 rounded-md border border-gray-200">
|
||||
{text.length} characters
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showValidation && text.trim() === '' && (
|
||||
<div className="mb-6 p-4 bg-red-50 border border-red-200 text-red-700 rounded-xl">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>⚠️</span>
|
||||
<p className="font-medium">Please enter some text before proceeding.</p>
|
||||
className="w-full h-96 p-6 border-2 border-gray-200 rounded-2xl focus:border-indigo-500 focus:ring-4 focus:ring-indigo-100 focus:outline-none resize-y text-base leading-relaxed transition-all duration-200 bg-gray-50 focus:bg-white"
|
||||
disabled={disabled || isLoading}
|
||||
/>
|
||||
<div className="absolute bottom-5 right-5 text-sm text-gray-500 bg-white px-4 py-2 rounded-xl border border-gray-200 shadow-sm font-medium">
|
||||
{text.length} characters
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleNext}
|
||||
disabled={isButtonDisabled}
|
||||
className="btn-primary"
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
|
||||
Processing...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-2">
|
||||
Continue
|
||||
<span>→</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{showValidation && text.trim() === '' && (
|
||||
<div className="mb-8 p-5 bg-red-50 border-2 border-red-200 text-red-700 rounded-2xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl">⚠️</span>
|
||||
<p className="font-semibold">Please enter some text before proceeding.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleNext}
|
||||
disabled={isButtonDisabled}
|
||||
className="btn-primary"
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
|
||||
Processing...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-3">
|
||||
Continue
|
||||
<span className="text-lg">→</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -159,116 +159,118 @@ export default function VoiceAssignmentSection({
|
||||
const isGenerateDisabled = !allRolesAssigned || isGenerating;
|
||||
|
||||
return (
|
||||
<div className="card-modern p-8 space-y-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Assign Voices</h2>
|
||||
<p className="text-gray-600">Choose a voice for each character in your story.</p>
|
||||
</div>
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="card-modern p-10 sm:p-12 space-y-10">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-3">Assign Voices</h2>
|
||||
<p className="text-lg text-gray-600">Choose a voice for each character in your story.</p>
|
||||
</div>
|
||||
|
||||
{/* Voice Assignments */}
|
||||
<div className="space-y-4">
|
||||
{roles.map((role, index) => (
|
||||
<div key={role.id} className="flex flex-col sm:flex-row sm:items-center gap-3 p-4 bg-gray-50 rounded-xl border border-gray-200">
|
||||
<div className="flex items-center gap-3 sm:w-48">
|
||||
<div className="flex items-center justify-center w-8 h-8 bg-indigo-100 text-indigo-700 rounded-lg font-semibold text-sm">
|
||||
{index + 1}
|
||||
{/* Voice Assignments */}
|
||||
<div className="space-y-5">
|
||||
{roles.map((role, index) => (
|
||||
<div key={role.id} className="flex flex-col sm:flex-row sm:items-center gap-4 p-6 bg-gradient-to-br from-gray-50 to-white rounded-2xl border-2 border-gray-200 hover:border-indigo-300 transition-all duration-200">
|
||||
<div className="flex items-center gap-4 sm:w-56">
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 text-white rounded-xl font-bold text-sm shadow-lg shadow-indigo-500/30">
|
||||
{index + 1}
|
||||
</div>
|
||||
<label className="font-bold text-gray-900 text-lg">
|
||||
{role.label}
|
||||
</label>
|
||||
</div>
|
||||
<label className="font-semibold text-gray-900">
|
||||
{role.label}
|
||||
</label>
|
||||
<select
|
||||
value={voiceAssignments.get(role.id) || ''}
|
||||
onChange={(e) => handleVoiceSelect(role.id, e.target.value)}
|
||||
className="flex-1 p-4 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-4 focus:ring-indigo-100 focus:outline-none bg-white text-base font-medium transition-all duration-200"
|
||||
>
|
||||
<option value="">Select a voice...</option>
|
||||
{KOKORO_VOICES.map((voice) => (
|
||||
<option key={voice.id} value={voice.id}>
|
||||
{voice.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Settings Grid */}
|
||||
<div className="grid sm:grid-cols-2 gap-8 pt-4">
|
||||
{/* Language */}
|
||||
<div>
|
||||
<label className="block text-base font-bold text-gray-900 mb-3">
|
||||
Language
|
||||
</label>
|
||||
<select
|
||||
value={voiceAssignments.get(role.id) || ''}
|
||||
onChange={(e) => handleVoiceSelect(role.id, e.target.value)}
|
||||
className="flex-1 p-3 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:outline-none bg-white"
|
||||
value={language}
|
||||
onChange={handleLanguageSelect}
|
||||
className="w-full p-4 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-4 focus:ring-indigo-100 focus:outline-none bg-white text-base font-medium transition-all duration-200"
|
||||
>
|
||||
<option value="">Select a voice...</option>
|
||||
{KOKORO_VOICES.map((voice) => (
|
||||
<option key={voice.id} value={voice.id}>
|
||||
{voice.label}
|
||||
{LANGUAGES.map((lang) => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Settings Grid */}
|
||||
<div className="grid sm:grid-cols-2 gap-6">
|
||||
{/* Language */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-900 mb-2">
|
||||
Language
|
||||
</label>
|
||||
<select
|
||||
value={language}
|
||||
onChange={handleLanguageSelect}
|
||||
className="w-full p-3 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:outline-none bg-white"
|
||||
>
|
||||
{LANGUAGES.map((lang) => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Speed */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-900 mb-2">
|
||||
Playback Speed: {speed.toFixed(1)}x
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="1.5"
|
||||
step="0.1"
|
||||
value={speed}
|
||||
onChange={handleSpeedChange}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>0.5x</span>
|
||||
<span>1.5x</span>
|
||||
{/* Speed */}
|
||||
<div>
|
||||
<label className="block text-base font-bold text-gray-900 mb-3">
|
||||
Playback Speed: <span className="text-indigo-600">{speed.toFixed(1)}x</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="1.5"
|
||||
step="0.1"
|
||||
value={speed}
|
||||
onChange={handleSpeedChange}
|
||||
className="w-full h-3"
|
||||
/>
|
||||
<div className="flex justify-between text-sm text-gray-500 mt-2 font-medium">
|
||||
<span>0.5x</span>
|
||||
<span>1.5x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Validation Message */}
|
||||
{!allRolesAssigned && !isGenerating && (
|
||||
<div className="p-4 bg-yellow-50 border border-yellow-200 text-yellow-800 rounded-xl">
|
||||
<p className="text-sm font-medium">Please assign voices to all roles before continuing.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-end">
|
||||
{onSkipReview && (
|
||||
<button
|
||||
onClick={onSkipReview}
|
||||
disabled={isGenerateDisabled}
|
||||
className="px-6 py-3 border-2 border-gray-300 text-gray-700 font-semibold rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
Skip Review & Generate
|
||||
</button>
|
||||
{/* Validation Message */}
|
||||
{!allRolesAssigned && !isGenerating && (
|
||||
<div className="p-5 bg-yellow-50 border-2 border-yellow-200 text-yellow-900 rounded-2xl">
|
||||
<p className="font-semibold">⚠️ Please assign voices to all roles before continuing.</p>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={onGenerate}
|
||||
disabled={isGenerateDisabled}
|
||||
className="btn-primary"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
|
||||
Generating...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-2">
|
||||
Continue
|
||||
<span>→</span>
|
||||
</span>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-end pt-4">
|
||||
{onSkipReview && (
|
||||
<button
|
||||
onClick={onSkipReview}
|
||||
disabled={isGenerateDisabled}
|
||||
className="px-8 py-4 border-2 border-gray-300 text-gray-700 font-semibold rounded-xl hover:bg-gray-50 hover:border-gray-400 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
|
||||
>
|
||||
Skip Review & Generate
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={onGenerate}
|
||||
disabled={isGenerateDisabled}
|
||||
className="btn-primary"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<span className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
|
||||
Generating...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-3">
|
||||
Continue
|
||||
<span className="text-lg">→</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,54 +2,66 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
background: #f8f9fa;
|
||||
background: linear-gradient(180deg, #f9fafb 0%, #f3f4f6 100%);
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.card-modern {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card-modern:hover {
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 12px 32px;
|
||||
border-radius: 12px;
|
||||
padding: 14px 40px;
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.4);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.4);
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.5);
|
||||
}
|
||||
|
||||
.btn-primary:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user