source: chapterx-frontend/src/pages/writer/EditStoryPage.tsx@ b62cefc

main
Last change on this file since b62cefc was b62cefc, checked in by kikisrbinoska <srbinoskakristina07@…>, 4 months ago

Added frontend and imporoved AI Suggestions service

  • Property mode set to 100644
File size: 8.6 KB
Line 
1import React, { useState, useEffect } from 'react'
2import { useParams, useNavigate } from 'react-router-dom'
3import { ArrowLeft, Plus, Save, Globe, Archive, Trash2 } from 'lucide-react'
4import { useAuthStore } from '../../store/authStore'
5import { useStoryStore } from '../../store/storyStore'
6import { useUIStore } from '../../store/uiStore'
7import { Button } from '../../components/ui/Button'
8import { ChapterList } from '../../components/story/ChapterList'
9import { CollaboratorManager } from '../../components/writer/CollaboratorManager'
10import { Modal } from '../../components/ui/Modal'
11
12const ALL_GENRES = ['Fantasy', 'Sci-Fi', 'Romance', 'Historical Fiction', 'Adventure', 'Thriller', 'Mystery', 'Horror', 'Contemporary', 'Poetry']
13
14export 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}
Note: See TracBrowser for help on using the repository browser.