| 1 | import React, { useState } from 'react'
|
|---|
| 2 | import { useParams, useNavigate } from 'react-router-dom'
|
|---|
| 3 | import { ArrowLeft, BookOpen } from 'lucide-react'
|
|---|
| 4 | import { useAuthStore } from '../../store/authStore'
|
|---|
| 5 | import { useStoryStore } from '../../store/storyStore'
|
|---|
| 6 | import { useUIStore } from '../../store/uiStore'
|
|---|
| 7 | import { Button } from '../../components/ui/Button'
|
|---|
| 8 | import { Chapter } from '../../types'
|
|---|
| 9 |
|
|---|
| 10 | export const CreateChapterPage: React.FC = () => {
|
|---|
| 11 | const { storyId } = useParams<{ storyId: string }>()
|
|---|
| 12 | const navigate = useNavigate()
|
|---|
| 13 | const { currentUser } = useAuthStore()
|
|---|
| 14 | const { stories, chapters, addChapter } = useStoryStore()
|
|---|
| 15 | const { addToast } = useUIStore()
|
|---|
| 16 | const [form, setForm] = useState({ title: '', content: '' })
|
|---|
| 17 | const [errors, setErrors] = useState<Record<string, string>>({})
|
|---|
| 18 | const [loading, setLoading] = useState(false)
|
|---|
| 19 |
|
|---|
| 20 | const story = stories.find(s => s.story_id === Number(storyId))
|
|---|
| 21 | const storyChapters = chapters.filter(c => c.story_id === Number(storyId))
|
|---|
| 22 | const wordCount = form.content.trim().split(/\s+/).filter(Boolean).length
|
|---|
| 23 |
|
|---|
| 24 | if (!story) {
|
|---|
| 25 | return (
|
|---|
| 26 | <div className="max-w-4xl mx-auto px-4 py-20 text-center">
|
|---|
| 27 | <h2 className="text-2xl text-white mb-4">Story not found</h2>
|
|---|
| 28 | <Button onClick={() => navigate('/writer')}>Back</Button>
|
|---|
| 29 | </div>
|
|---|
| 30 | )
|
|---|
| 31 | }
|
|---|
| 32 |
|
|---|
| 33 | const validate = () => {
|
|---|
| 34 | const e: Record<string, string> = {}
|
|---|
| 35 | if (!form.title.trim()) e.title = 'Chapter title is required'
|
|---|
| 36 | if (!form.content.trim()) e.content = 'Chapter content is required'
|
|---|
| 37 | if (wordCount < 10) e.content = 'Chapter must be at least 10 words'
|
|---|
| 38 | setErrors(e)
|
|---|
| 39 | return Object.keys(e).length === 0
|
|---|
| 40 | }
|
|---|
| 41 |
|
|---|
| 42 | const handleSubmit = async (isPublished: boolean) => {
|
|---|
| 43 | if (!validate() || !currentUser) return
|
|---|
| 44 | setLoading(true)
|
|---|
| 45 | await new Promise(r => setTimeout(r, 400))
|
|---|
| 46 | const chapter: Chapter = {
|
|---|
| 47 | chapter_id: Date.now(),
|
|---|
| 48 | story_id: Number(storyId),
|
|---|
| 49 | title: form.title.trim(),
|
|---|
| 50 | content: form.content.trim(),
|
|---|
| 51 | chapter_number: storyChapters.length + 1,
|
|---|
| 52 | word_count: wordCount,
|
|---|
| 53 | view_count: 0,
|
|---|
| 54 | created_at: new Date().toISOString(),
|
|---|
| 55 | updated_at: new Date().toISOString(),
|
|---|
| 56 | is_published: isPublished,
|
|---|
| 57 | }
|
|---|
| 58 | try {
|
|---|
| 59 | await addChapter(chapter)
|
|---|
| 60 | } catch (err: any) {
|
|---|
| 61 | const msg = err?.response?.data?.message || err?.message || 'Failed to save chapter.'
|
|---|
| 62 | addToast(msg, 'error')
|
|---|
| 63 | setLoading(false)
|
|---|
| 64 | return
|
|---|
| 65 | }
|
|---|
| 66 | addToast(isPublished ? 'Chapter published!' : 'Chapter saved as draft!')
|
|---|
| 67 | navigate(isPublished ? `/story/${storyId}` : `/writer/edit-story/${storyId}`)
|
|---|
| 68 | setLoading(false)
|
|---|
| 69 | }
|
|---|
| 70 |
|
|---|
| 71 | return (
|
|---|
| 72 | <div className="max-w-3xl mx-auto px-4 py-8">
|
|---|
| 73 | <div className="flex items-center gap-3 mb-8">
|
|---|
| 74 | <button onClick={() => navigate(`/writer/edit-story/${storyId}`)} className="text-slate-400 hover:text-white transition-colors">
|
|---|
| 75 | <ArrowLeft size={20} />
|
|---|
| 76 | </button>
|
|---|
| 77 | <div>
|
|---|
| 78 | <p className="text-slate-400 text-sm">{story.title}</p>
|
|---|
| 79 | <h1 className="font-serif text-2xl font-bold text-white">New Chapter</h1>
|
|---|
| 80 | <p className="text-slate-500 text-xs mt-0.5">Chapter {storyChapters.length + 1}</p>
|
|---|
| 81 | </div>
|
|---|
| 82 | </div>
|
|---|
| 83 |
|
|---|
| 84 | <div className="space-y-6">
|
|---|
| 85 | <div>
|
|---|
| 86 | <label className="block text-sm font-medium text-slate-300 mb-1.5">Chapter Title *</label>
|
|---|
| 87 | <input
|
|---|
| 88 | value={form.title}
|
|---|
| 89 | onChange={e => { setForm(f => ({ ...f, title: e.target.value })); setErrors(e => ({ ...e, title: '' })) }}
|
|---|
| 90 | placeholder="The Awakening..."
|
|---|
| 91 | className={`w-full px-4 py-3 bg-slate-800 border rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 font-serif text-lg ${errors.title ? 'border-rose-500' : 'border-slate-700'}`}
|
|---|
| 92 | />
|
|---|
| 93 | {errors.title && <p className="text-rose-400 text-xs mt-1">{errors.title}</p>}
|
|---|
| 94 | </div>
|
|---|
| 95 |
|
|---|
| 96 | <div>
|
|---|
| 97 | <div className="flex items-center justify-between mb-1.5">
|
|---|
| 98 | <label className="text-sm font-medium text-slate-300">Chapter Content *</label>
|
|---|
| 99 | <div className="flex items-center gap-2 text-xs text-slate-500">
|
|---|
| 100 | <BookOpen size={12} />
|
|---|
| 101 | <span className={wordCount > 100 ? 'text-emerald-400' : wordCount > 50 ? 'text-amber-400' : 'text-slate-500'}>
|
|---|
| 102 | {wordCount.toLocaleString()} words
|
|---|
| 103 | </span>
|
|---|
| 104 | </div>
|
|---|
| 105 | </div>
|
|---|
| 106 | <textarea
|
|---|
| 107 | value={form.content}
|
|---|
| 108 | onChange={e => { setForm(f => ({ ...f, content: e.target.value })); setErrors(e => ({ ...e, content: '' })) }}
|
|---|
| 109 | placeholder="Begin your chapter here..."
|
|---|
| 110 | rows={20}
|
|---|
| 111 | className={`w-full px-4 py-3 bg-slate-800 border rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 resize-y font-serif text-base leading-relaxed ${errors.content ? 'border-rose-500' : 'border-slate-700'}`}
|
|---|
| 112 | />
|
|---|
| 113 | {errors.content && <p className="text-rose-400 text-xs mt-1">{errors.content}</p>}
|
|---|
| 114 | </div>
|
|---|
| 115 |
|
|---|
| 116 | <div className="flex gap-3 pt-4 border-t border-slate-700">
|
|---|
| 117 | <Button variant="secondary" className="flex-1" onClick={() => handleSubmit(false)} loading={loading}>
|
|---|
| 118 | Save as Draft
|
|---|
| 119 | </Button>
|
|---|
| 120 | <Button className="flex-1" onClick={() => handleSubmit(true)} loading={loading}>
|
|---|
| 121 | Publish Chapter
|
|---|
| 122 | </Button>
|
|---|
| 123 | </div>
|
|---|
| 124 | </div>
|
|---|
| 125 | </div>
|
|---|
| 126 | )
|
|---|
| 127 | }
|
|---|