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

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

Added fixes for the login,stories management and reading lists

  • Property mode set to 100644
File size: 8.7 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 navigate(`/story/${story.story_id}`)
66 }
67
68 const handleDelete = () => {
69 deleteStory(story.story_id)
70 addToast('Story deleted', 'info')
71 navigate('/writer')
72 }
73
74 const toggleGenre = (g: string) =>
75 setForm(f => ({
76 ...f,
77 genres: f.genres.includes(g) ? f.genres.filter(x => x !== g) : [...f.genres, g],
78 }))
79
80 return (
81 <div className="max-w-5xl mx-auto px-4 py-8">
82 {/* Header */}
83 <div className="flex items-center justify-between mb-8">
84 <div className="flex items-center gap-3">
85 <button onClick={() => navigate('/writer')} className="text-slate-400 hover:text-white transition-colors">
86 <ArrowLeft size={20} />
87 </button>
88 <div>
89 <h1 className="font-serif text-2xl font-bold text-white">Edit Story</h1>
90 <p className="text-slate-400 text-sm mt-0.5">Status: <span className="capitalize text-white">{story.status}</span></p>
91 </div>
92 </div>
93 <div className="flex gap-2">
94 {story.status === 'draft' && (
95 <Button size="sm" variant="secondary" onClick={() => { updateStoryStatus(story.story_id, 'published'); addToast('Story published!') }}>
96 <Globe size={14} />
97 Publish
98 </Button>
99 )}
100 {story.status === 'published' && (
101 <Button size="sm" variant="ghost" onClick={() => { updateStoryStatus(story.story_id, 'archived'); addToast('Story archived', 'info') }}>
102 <Archive size={14} />
103 Archive
104 </Button>
105 )}
106 <Button size="sm" variant="danger" onClick={() => setDeleteConfirm(true)}>
107 <Trash2 size={14} />
108 </Button>
109 <Button size="sm" onClick={handleSave} loading={saving}>
110 <Save size={14} />
111 Save
112 </Button>
113 </div>
114 </div>
115
116 <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
117 {/* Form */}
118 <div className="lg:col-span-2 space-y-6">
119 <div>
120 <label className="block text-sm font-medium text-slate-300 mb-1.5">Title</label>
121 <input
122 value={form.title}
123 onChange={e => setForm(f => ({ ...f, title: e.target.value }))}
124 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"
125 />
126 </div>
127 <div>
128 <label className="block text-sm font-medium text-slate-300 mb-1.5">Short Description</label>
129 <textarea
130 value={form.short_description}
131 onChange={e => setForm(f => ({ ...f, short_description: e.target.value }))}
132 rows={3}
133 maxLength={280}
134 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"
135 />
136 </div>
137 <div>
138 <label className="block text-sm font-medium text-slate-300 mb-1.5">Story Content</label>
139 <textarea
140 value={form.content}
141 onChange={e => setForm(f => ({ ...f, content: e.target.value }))}
142 rows={6}
143 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"
144 />
145 </div>
146 <div>
147 <label className="block text-sm font-medium text-slate-300 mb-2">Genres</label>
148 <div className="flex flex-wrap gap-2">
149 {ALL_GENRES.map(g => (
150 <button
151 key={g}
152 type="button"
153 onClick={() => toggleGenre(g)}
154 className={`px-3 py-1.5 rounded-full text-sm border transition-all ${
155 form.genres.includes(g)
156 ? 'bg-indigo-500/30 text-indigo-300 border-indigo-500/50'
157 : 'bg-slate-800 text-slate-400 border-slate-700 hover:border-slate-500'
158 }`}
159 >
160 {g}
161 </button>
162 ))}
163 </div>
164 </div>
165 <div className="flex items-center gap-3">
166 <button
167 type="button"
168 onClick={() => setForm(f => ({ ...f, mature_content: !f.mature_content }))}
169 className={`w-11 h-6 rounded-full transition-colors ${form.mature_content ? 'bg-rose-500' : 'bg-slate-600'} relative`}
170 >
171 <div className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${form.mature_content ? 'translate-x-6' : 'translate-x-1'}`} />
172 </button>
173 <span className="text-sm text-slate-300">Mature Content (18+)</span>
174 </div>
175 </div>
176
177 {/* Sidebar */}
178 <div className="space-y-6">
179 {/* Chapters */}
180 <div className="bg-slate-800 border border-slate-700 rounded-2xl p-5">
181 <div className="flex items-center justify-between mb-4">
182 <h3 className="text-white font-semibold">Chapters</h3>
183 <Button size="sm" onClick={() => navigate(`/writer/create-chapter/${story.story_id}`)}>
184 <Plus size={13} />
185 Add
186 </Button>
187 </div>
188 <ChapterList
189 chapters={storyChapters}
190 storyId={story.story_id}
191 showEditLinks
192 onEdit={cid => navigate(`/writer/edit-chapter/${cid}`)}
193 />
194 </div>
195
196 {/* Collaborators */}
197 <CollaboratorManager
198 storyId={story.story_id}
199 storyTitle={story.title}
200 ownerId={story.user_id}
201 />
202 </div>
203 </div>
204
205 {/* Delete confirm */}
206 <Modal isOpen={deleteConfirm} onClose={() => setDeleteConfirm(false)} title="Delete Story" size="sm">
207 <div className="space-y-4">
208 <p className="text-slate-300 text-sm">
209 Are you sure you want to permanently delete "{story.title}"? This cannot be undone.
210 </p>
211 <div className="flex gap-3">
212 <Button variant="secondary" className="flex-1" onClick={() => setDeleteConfirm(false)}>Cancel</Button>
213 <Button variant="danger" className="flex-1" onClick={handleDelete}>
214 <Trash2 size={14} />
215 Delete
216 </Button>
217 </div>
218 </div>
219 </Modal>
220 </div>
221 )
222}
Note: See TracBrowser for help on using the repository browser.