source: chapterx-frontend/src/pages/writer/CreateStoryPage.tsx@ 99c1e45

main
Last change on this file since 99c1e45 was 99c1e45, checked in by kikisrbinoska <srbinoskakristina07@…>, 11 days ago

Fixed writer section and admin management

  • Property mode set to 100644
File size: 10.0 KB
Line 
1import React, { useState, useRef } from 'react'
2import { useNavigate } from 'react-router-dom'
3import { Feather, ArrowLeft, X } 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 { StoryCreationAIPanel, StoryCreationAIPanelRef } from '../../components/writer/StoryCreationAIPanel'
9import { Story } from '../../types'
10
11const ALL_GENRES = ['Fantasy', 'Sci-Fi', 'Romance', 'Historical Fiction', 'Adventure', 'Thriller', 'Mystery', 'Horror', 'Contemporary', 'Poetry']
12
13export const CreateStoryPage: React.FC = () => {
14 const navigate = useNavigate()
15 const { currentUser } = useAuthStore()
16 const { addStory, addSuggestion } = useStoryStore()
17 const { addToast } = useUIStore()
18 const aiPanelRef = useRef<StoryCreationAIPanelRef>(null)
19 const [form, setForm] = useState({
20 title: '',
21 short_description: '',
22 content: '',
23 cover_image: '',
24 genres: [] as string[],
25 mature_content: false,
26 })
27 const [errors, setErrors] = useState<Record<string, string>>({})
28 const [loading, setLoading] = useState(false)
29
30 const validate = () => {
31 const e: Record<string, string> = {}
32 if (!form.title.trim()) e.title = 'Title is required'
33 if (!form.short_description.trim()) e.short_description = 'Description is required'
34 if (form.short_description.length > 280) e.short_description = 'Max 280 characters'
35 if (!form.content.trim()) e.content = 'Story content is required'
36 if (form.genres.length === 0) e.genres = 'Select at least one genre'
37 setErrors(e)
38 return Object.keys(e).length === 0
39 }
40
41 const handleSubmit = async (status: 'draft' | 'published') => {
42 if (!validate() || !currentUser) return
43 setLoading(true)
44 await new Promise(r => setTimeout(r, 500))
45 const story: Story = {
46 story_id: Date.now(),
47 ...form,
48 cover_image: form.cover_image || undefined,
49 user_id: currentUser.user_id,
50 author_username: currentUser.username,
51 status,
52 created_at: new Date().toISOString(),
53 updated_at: new Date().toISOString(),
54 total_likes: 0,
55 total_comments: 0,
56 total_chapters: 0,
57 total_views: 0,
58 }
59 let realId: number
60 try {
61 realId = await addStory(story)
62 } catch (err: any) {
63 const msg = err?.response?.data?.message || err?.message || 'Failed to save story.'
64 addToast(msg, 'error')
65 setLoading(false)
66 return
67 }
68
69 addToast(status === 'published' ? 'Story published!' : 'Draft saved!')
70 navigate(`/writer/edit-story/${realId}`)
71 setLoading(false)
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 setErrors(e => ({ ...e, genres: '' }))
80 }
81
82 const setField = (field: string, value: string | boolean) => {
83 setForm(f => ({ ...f, [field]: value }))
84 setErrors(e => ({ ...e, [field]: '' }))
85 }
86
87 return (
88 <div className="max-w-6xl mx-auto px-4 py-8">
89 <div className="flex items-center gap-3 mb-8">
90 <button onClick={() => navigate('/writer')} className="text-slate-400 hover:text-white transition-colors">
91 <ArrowLeft size={20} />
92 </button>
93 <div>
94 <h1 className="font-serif text-2xl font-bold text-white">Create New Story</h1>
95 <p className="text-slate-400 text-sm mt-0.5">Tell your story to the world</p>
96 </div>
97 </div>
98
99 <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
100 <div className="lg:col-span-2 space-y-6">
101 {/* Title */}
102 <div>
103 <label className="block text-sm font-medium text-slate-300 mb-1.5">Story Title *</label>
104 <input
105 value={form.title}
106 onChange={e => setField('title', e.target.value)}
107 placeholder="The Chronicles of Eldoria..."
108 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 text-lg font-serif ${errors.title ? 'border-rose-500' : 'border-slate-700'}`}
109 />
110 {errors.title && <p className="text-rose-400 text-xs mt-1">{errors.title}</p>}
111 </div>
112
113 {/* Short description */}
114 <div>
115 <label className="block text-sm font-medium text-slate-300 mb-1.5">Short Description * <span className="text-slate-500 font-normal">(shown on story cards)</span></label>
116 <textarea
117 value={form.short_description}
118 onChange={e => setField('short_description', e.target.value)}
119 placeholder="When the last dragon awakens..."
120 rows={3}
121 maxLength={280}
122 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-none ${errors.short_description ? 'border-rose-500' : 'border-slate-700'}`}
123 />
124 <div className="flex justify-between items-center mt-1">
125 {errors.short_description ? <p className="text-rose-400 text-xs">{errors.short_description}</p> : <span />}
126 <span className="text-slate-600 text-xs">{form.short_description.length}/280</span>
127 </div>
128 </div>
129
130 {/* Cover image */}
131 <div>
132 <label className="block text-sm font-medium text-slate-300 mb-1.5">Cover Image URL <span className="text-slate-500 font-normal">(optional)</span></label>
133 <input
134 value={form.cover_image}
135 onChange={e => {
136 const val = e.target.value
137 if (!val || val.startsWith('http')) setField('cover_image', val)
138 }}
139 placeholder="https://example.com/image.jpg"
140 className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500"
141 />
142 {form.cover_image?.startsWith('http') && (
143 <img src={form.cover_image} alt="Cover preview" className="mt-2 h-32 w-full object-cover rounded-xl opacity-80" onError={e => (e.currentTarget.style.display = 'none')} />
144 )}
145 </div>
146
147 {/* Content */}
148 <div>
149 <label className="block text-sm font-medium text-slate-300 mb-1.5">Story Content *</label>
150 <textarea
151 value={form.content}
152 onChange={e => setField('content', e.target.value)}
153 placeholder="Begin your story here. This is the introduction that readers will see..."
154 rows={8}
155 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 ${errors.content ? 'border-rose-500' : 'border-slate-700'}`}
156 />
157 {errors.content && <p className="text-rose-400 text-xs mt-1">{errors.content}</p>}
158 </div>
159
160 {/* Genres */}
161 <div>
162 <label className="block text-sm font-medium text-slate-300 mb-2">Genres * <span className="text-slate-500 font-normal">(select 1-3)</span></label>
163 <div className="flex flex-wrap gap-2 mb-2">
164 {ALL_GENRES.map(g => (
165 <button
166 key={g}
167 type="button"
168 onClick={() => toggleGenre(g)}
169 className={`px-3 py-1.5 rounded-full text-sm border transition-all ${
170 form.genres.includes(g)
171 ? 'bg-indigo-500/30 text-indigo-300 border-indigo-500/50'
172 : 'bg-slate-800 text-slate-400 border-slate-700 hover:border-slate-500'
173 }`}
174 >
175 {form.genres.includes(g) && <span className="mr-1">✓</span>}
176 {g}
177 </button>
178 ))}
179 </div>
180 {form.genres.length > 0 && (
181 <div className="flex flex-wrap gap-1 mt-1">
182 {form.genres.map(g => (
183 <span key={g} className="flex items-center gap-1 text-xs px-2 py-0.5 bg-indigo-500/20 text-indigo-300 rounded-full">
184 {g}
185 <button onClick={() => toggleGenre(g)}><X size={10} /></button>
186 </span>
187 ))}
188 </div>
189 )}
190 {errors.genres && <p className="text-rose-400 text-xs mt-1">{errors.genres}</p>}
191 </div>
192
193 {/* Mature content */}
194 <div className="flex items-center gap-3">
195 <button
196 type="button"
197 onClick={() => setField('mature_content', !form.mature_content)}
198 className={`w-11 h-6 rounded-full transition-colors ${form.mature_content ? 'bg-rose-500' : 'bg-slate-600'} relative flex-shrink-0`}
199 >
200 <div className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${form.mature_content ? 'translate-x-6' : 'translate-x-1'}`} />
201 </button>
202 <div>
203 <p className="text-white text-sm font-medium">Mature Content (18+)</p>
204 <p className="text-slate-500 text-xs">Enable for stories with adult themes</p>
205 </div>
206 </div>
207
208 {/* Actions */}
209 <div className="flex gap-3 pt-4 border-t border-slate-700">
210 <Button variant="secondary" className="flex-1" onClick={() => handleSubmit('draft')} loading={loading}>
211 Save as Draft
212 </Button>
213 <Button className="flex-1" onClick={() => handleSubmit('published')} loading={loading}>
214 <Feather size={16} />
215 Publish Story
216 </Button>
217 </div>
218 </div>
219
220 {/* AI Suggestions Sidebar */}
221 <div className="lg:col-span-1">
222 <StoryCreationAIPanel
223 ref={aiPanelRef}
224 title={form.title}
225 description={form.short_description}
226 content={form.content}
227 genres={form.genres}
228 />
229 </div>
230 </div>
231 </div>
232 )
233}
Note: See TracBrowser for help on using the repository browser.