| 1 | import React, { useState, useEffect } from 'react'
|
|---|
| 2 | import { useParams, useNavigate } from 'react-router-dom'
|
|---|
| 3 | import { ArrowLeft, Plus, Save, Globe, Archive, Trash2 } 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 { ChapterList } from '../../components/story/ChapterList'
|
|---|
| 9 | import { CollaboratorManager } from '../../components/writer/CollaboratorManager'
|
|---|
| 10 | import { Modal } from '../../components/ui/Modal'
|
|---|
| 11 |
|
|---|
| 12 | const ALL_GENRES = ['Fantasy', 'Sci-Fi', 'Romance', 'Historical Fiction', 'Adventure', 'Thriller', 'Mystery', 'Horror', 'Contemporary', 'Poetry']
|
|---|
| 13 |
|
|---|
| 14 | export const EditStoryPage: React.FC = () => {
|
|---|
| 15 | const { id } = useParams<{ id: string }>()
|
|---|
| 16 | const navigate = useNavigate()
|
|---|
| 17 | const { currentUser } = useAuthStore()
|
|---|
| 18 | const { stories, chapters, updateStory, deleteStory, updateStoryStatus } = useStoryStore()
|
|---|
| 19 | const { addToast } = useUIStore()
|
|---|
| 20 | const [deleteConfirm, setDeleteConfirm] = useState(false)
|
|---|
| 21 | const [saving, setSaving] = useState(false)
|
|---|
| 22 |
|
|---|
| 23 | const story = stories.find(s => s.story_id === Number(id))
|
|---|
| 24 |
|
|---|
| 25 | const [form, setForm] = useState({
|
|---|
| 26 | title: '',
|
|---|
| 27 | short_description: '',
|
|---|
| 28 | content: '',
|
|---|
| 29 | genres: [] as string[],
|
|---|
| 30 | mature_content: false,
|
|---|
| 31 | })
|
|---|
| 32 |
|
|---|
| 33 | useEffect(() => {
|
|---|
| 34 | if (story) {
|
|---|
| 35 | setForm({
|
|---|
| 36 | title: story.title,
|
|---|
| 37 | short_description: story.short_description,
|
|---|
| 38 | content: story.content,
|
|---|
| 39 | genres: [...story.genres],
|
|---|
| 40 | mature_content: story.mature_content,
|
|---|
| 41 | })
|
|---|
| 42 | }
|
|---|
| 43 | }, [story?.story_id])
|
|---|
| 44 |
|
|---|
| 45 | if (!story) {
|
|---|
| 46 | return (
|
|---|
| 47 | <div className="max-w-4xl mx-auto px-4 py-20 text-center">
|
|---|
| 48 | <h2 className="text-2xl text-white mb-4">Story not found</h2>
|
|---|
| 49 | <Button onClick={() => navigate('/writer')}>Back to Dashboard</Button>
|
|---|
| 50 | </div>
|
|---|
| 51 | )
|
|---|
| 52 | }
|
|---|
| 53 |
|
|---|
| 54 | const canEdit = currentUser?.user_id === story.user_id || currentUser?.role === 'admin'
|
|---|
| 55 | if (!canEdit) navigate('/writer')
|
|---|
| 56 |
|
|---|
| 57 | const storyChapters = chapters.filter(c => c.story_id === story.story_id)
|
|---|
| 58 |
|
|---|
| 59 | const handleSave = async () => {
|
|---|
| 60 | setSaving(true)
|
|---|
| 61 | await new Promise(r => setTimeout(r, 400))
|
|---|
| 62 | updateStory(story.story_id, { ...form, updated_at: new Date().toISOString() })
|
|---|
| 63 | addToast('Story updated!')
|
|---|
| 64 | setSaving(false)
|
|---|
| 65 | }
|
|---|
| 66 |
|
|---|
| 67 | const handleDelete = () => {
|
|---|
| 68 | deleteStory(story.story_id)
|
|---|
| 69 | addToast('Story deleted', 'info')
|
|---|
| 70 | navigate('/writer')
|
|---|
| 71 | }
|
|---|
| 72 |
|
|---|
| 73 | const toggleGenre = (g: string) =>
|
|---|
| 74 | setForm(f => ({
|
|---|
| 75 | ...f,
|
|---|
| 76 | genres: f.genres.includes(g) ? f.genres.filter(x => x !== g) : [...f.genres, g],
|
|---|
| 77 | }))
|
|---|
| 78 |
|
|---|
| 79 | return (
|
|---|
| 80 | <div className="max-w-5xl mx-auto px-4 py-8">
|
|---|
| 81 | {/* Header */}
|
|---|
| 82 | <div className="flex items-center justify-between mb-8">
|
|---|
| 83 | <div className="flex items-center gap-3">
|
|---|
| 84 | <button onClick={() => navigate('/writer')} className="text-slate-400 hover:text-white transition-colors">
|
|---|
| 85 | <ArrowLeft size={20} />
|
|---|
| 86 | </button>
|
|---|
| 87 | <div>
|
|---|
| 88 | <h1 className="font-serif text-2xl font-bold text-white">Edit Story</h1>
|
|---|
| 89 | <p className="text-slate-400 text-sm mt-0.5">Status: <span className="capitalize text-white">{story.status}</span></p>
|
|---|
| 90 | </div>
|
|---|
| 91 | </div>
|
|---|
| 92 | <div className="flex gap-2">
|
|---|
| 93 | {story.status === 'draft' && (
|
|---|
| 94 | <Button size="sm" variant="secondary" onClick={() => { updateStoryStatus(story.story_id, 'published'); addToast('Story published!') }}>
|
|---|
| 95 | <Globe size={14} />
|
|---|
| 96 | Publish
|
|---|
| 97 | </Button>
|
|---|
| 98 | )}
|
|---|
| 99 | {story.status === 'published' && (
|
|---|
| 100 | <Button size="sm" variant="ghost" onClick={() => { updateStoryStatus(story.story_id, 'archived'); addToast('Story archived', 'info') }}>
|
|---|
| 101 | <Archive size={14} />
|
|---|
| 102 | Archive
|
|---|
| 103 | </Button>
|
|---|
| 104 | )}
|
|---|
| 105 | <Button size="sm" variant="danger" onClick={() => setDeleteConfirm(true)}>
|
|---|
| 106 | <Trash2 size={14} />
|
|---|
| 107 | </Button>
|
|---|
| 108 | <Button size="sm" onClick={handleSave} loading={saving}>
|
|---|
| 109 | <Save size={14} />
|
|---|
| 110 | Save
|
|---|
| 111 | </Button>
|
|---|
| 112 | </div>
|
|---|
| 113 | </div>
|
|---|
| 114 |
|
|---|
| 115 | <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|---|
| 116 | {/* Form */}
|
|---|
| 117 | <div className="lg:col-span-2 space-y-6">
|
|---|
| 118 | <div>
|
|---|
| 119 | <label className="block text-sm font-medium text-slate-300 mb-1.5">Title</label>
|
|---|
| 120 | <input
|
|---|
| 121 | value={form.title}
|
|---|
| 122 | onChange={e => setForm(f => ({ ...f, title: e.target.value }))}
|
|---|
| 123 | className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white focus:outline-none focus:border-indigo-500 font-serif text-lg"
|
|---|
| 124 | />
|
|---|
| 125 | </div>
|
|---|
| 126 | <div>
|
|---|
| 127 | <label className="block text-sm font-medium text-slate-300 mb-1.5">Short Description</label>
|
|---|
| 128 | <textarea
|
|---|
| 129 | value={form.short_description}
|
|---|
| 130 | onChange={e => setForm(f => ({ ...f, short_description: e.target.value }))}
|
|---|
| 131 | rows={3}
|
|---|
| 132 | maxLength={280}
|
|---|
| 133 | className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white focus:outline-none focus:border-indigo-500 resize-none"
|
|---|
| 134 | />
|
|---|
| 135 | </div>
|
|---|
| 136 | <div>
|
|---|
| 137 | <label className="block text-sm font-medium text-slate-300 mb-1.5">Story Content</label>
|
|---|
| 138 | <textarea
|
|---|
| 139 | value={form.content}
|
|---|
| 140 | onChange={e => setForm(f => ({ ...f, content: e.target.value }))}
|
|---|
| 141 | rows={6}
|
|---|
| 142 | className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white focus:outline-none focus:border-indigo-500 resize-y font-serif"
|
|---|
| 143 | />
|
|---|
| 144 | </div>
|
|---|
| 145 | <div>
|
|---|
| 146 | <label className="block text-sm font-medium text-slate-300 mb-2">Genres</label>
|
|---|
| 147 | <div className="flex flex-wrap gap-2">
|
|---|
| 148 | {ALL_GENRES.map(g => (
|
|---|
| 149 | <button
|
|---|
| 150 | key={g}
|
|---|
| 151 | type="button"
|
|---|
| 152 | onClick={() => toggleGenre(g)}
|
|---|
| 153 | className={`px-3 py-1.5 rounded-full text-sm border transition-all ${
|
|---|
| 154 | form.genres.includes(g)
|
|---|
| 155 | ? 'bg-indigo-500/30 text-indigo-300 border-indigo-500/50'
|
|---|
| 156 | : 'bg-slate-800 text-slate-400 border-slate-700 hover:border-slate-500'
|
|---|
| 157 | }`}
|
|---|
| 158 | >
|
|---|
| 159 | {g}
|
|---|
| 160 | </button>
|
|---|
| 161 | ))}
|
|---|
| 162 | </div>
|
|---|
| 163 | </div>
|
|---|
| 164 | <div className="flex items-center gap-3">
|
|---|
| 165 | <button
|
|---|
| 166 | type="button"
|
|---|
| 167 | onClick={() => setForm(f => ({ ...f, mature_content: !f.mature_content }))}
|
|---|
| 168 | className={`w-11 h-6 rounded-full transition-colors ${form.mature_content ? 'bg-rose-500' : 'bg-slate-600'} relative`}
|
|---|
| 169 | >
|
|---|
| 170 | <div className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${form.mature_content ? 'translate-x-6' : 'translate-x-1'}`} />
|
|---|
| 171 | </button>
|
|---|
| 172 | <span className="text-sm text-slate-300">Mature Content (18+)</span>
|
|---|
| 173 | </div>
|
|---|
| 174 | </div>
|
|---|
| 175 |
|
|---|
| 176 | {/* Sidebar */}
|
|---|
| 177 | <div className="space-y-6">
|
|---|
| 178 | {/* Chapters */}
|
|---|
| 179 | <div className="bg-slate-800 border border-slate-700 rounded-2xl p-5">
|
|---|
| 180 | <div className="flex items-center justify-between mb-4">
|
|---|
| 181 | <h3 className="text-white font-semibold">Chapters</h3>
|
|---|
| 182 | <Button size="sm" onClick={() => navigate(`/writer/create-chapter/${story.story_id}`)}>
|
|---|
| 183 | <Plus size={13} />
|
|---|
| 184 | Add
|
|---|
| 185 | </Button>
|
|---|
| 186 | </div>
|
|---|
| 187 | <ChapterList
|
|---|
| 188 | chapters={storyChapters}
|
|---|
| 189 | storyId={story.story_id}
|
|---|
| 190 | showEditLinks
|
|---|
| 191 | onEdit={cid => navigate(`/writer/edit-chapter/${cid}`)}
|
|---|
| 192 | />
|
|---|
| 193 | </div>
|
|---|
| 194 |
|
|---|
| 195 | {/* Collaborators */}
|
|---|
| 196 | <CollaboratorManager
|
|---|
| 197 | storyId={story.story_id}
|
|---|
| 198 | storyTitle={story.title}
|
|---|
| 199 | ownerId={story.user_id}
|
|---|
| 200 | />
|
|---|
| 201 | </div>
|
|---|
| 202 | </div>
|
|---|
| 203 |
|
|---|
| 204 | {/* Delete confirm */}
|
|---|
| 205 | <Modal isOpen={deleteConfirm} onClose={() => setDeleteConfirm(false)} title="Delete Story" size="sm">
|
|---|
| 206 | <div className="space-y-4">
|
|---|
| 207 | <p className="text-slate-300 text-sm">
|
|---|
| 208 | Are you sure you want to permanently delete "{story.title}"? This cannot be undone.
|
|---|
| 209 | </p>
|
|---|
| 210 | <div className="flex gap-3">
|
|---|
| 211 | <Button variant="secondary" className="flex-1" onClick={() => setDeleteConfirm(false)}>Cancel</Button>
|
|---|
| 212 | <Button variant="danger" className="flex-1" onClick={handleDelete}>
|
|---|
| 213 | <Trash2 size={14} />
|
|---|
| 214 | Delete
|
|---|
| 215 | </Button>
|
|---|
| 216 | </div>
|
|---|
| 217 | </div>
|
|---|
| 218 | </Modal>
|
|---|
| 219 | </div>
|
|---|
| 220 | )
|
|---|
| 221 | }
|
|---|