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

main
Last change on this file since acf690c 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: 9.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 genres: [] as string[],
24 mature_content: false,
25 })
26 const [errors, setErrors] = useState<Record<string, string>>({})
27 const [loading, setLoading] = useState(false)
28
29 const validate = () => {
30 const e: Record<string, string> = {}
31 if (!form.title.trim()) e.title = 'Title is required'
32 if (!form.short_description.trim()) e.short_description = 'Description is required'
33 if (form.short_description.length > 280) e.short_description = 'Max 280 characters'
34 if (!form.content.trim()) e.content = 'Story content is required'
35 if (form.genres.length === 0) e.genres = 'Select at least one genre'
36 setErrors(e)
37 return Object.keys(e).length === 0
38 }
39
40 const handleSubmit = async (status: 'draft' | 'published') => {
41 if (!validate() || !currentUser) return
42 setLoading(true)
43 await new Promise(r => setTimeout(r, 500))
44 const story: Story = {
45 story_id: Date.now(),
46 ...form,
47 user_id: currentUser.user_id,
48 author_username: currentUser.username,
49 status,
50 created_at: new Date().toISOString(),
51 updated_at: new Date().toISOString(),
52 total_likes: 0,
53 total_comments: 0,
54 total_chapters: 0,
55 total_views: 0,
56 }
57 let realId: number
58 try {
59 realId = await addStory(story)
60 } catch (err: any) {
61 const msg = err?.response?.data?.message || err?.message || 'Failed to save story.'
62 addToast(msg, 'error')
63 setLoading(false)
64 return
65 }
66
67 addToast(status === 'published' ? 'Story published!' : 'Draft saved!')
68 navigate(`/writer/edit-story/${realId}`)
69 setLoading(false)
70 }
71
72 const toggleGenre = (g: string) => {
73 setForm(f => ({
74 ...f,
75 genres: f.genres.includes(g) ? f.genres.filter(x => x !== g) : [...f.genres, g],
76 }))
77 setErrors(e => ({ ...e, genres: '' }))
78 }
79
80 const setField = (field: string, value: string | boolean) => {
81 setForm(f => ({ ...f, [field]: value }))
82 setErrors(e => ({ ...e, [field]: '' }))
83 }
84
85 return (
86 <div className="max-w-6xl mx-auto px-4 py-8">
87 <div className="flex items-center gap-3 mb-8">
88 <button onClick={() => navigate('/writer')} className="text-slate-400 hover:text-white transition-colors">
89 <ArrowLeft size={20} />
90 </button>
91 <div>
92 <h1 className="font-serif text-2xl font-bold text-white">Create New Story</h1>
93 <p className="text-slate-400 text-sm mt-0.5">Tell your story to the world</p>
94 </div>
95 </div>
96
97 <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
98 <div className="lg:col-span-2 space-y-6">
99 {/* Title */}
100 <div>
101 <label className="block text-sm font-medium text-slate-300 mb-1.5">Story Title *</label>
102 <input
103 value={form.title}
104 onChange={e => setField('title', e.target.value)}
105 placeholder="The Chronicles of Eldoria..."
106 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'}`}
107 />
108 {errors.title && <p className="text-rose-400 text-xs mt-1">{errors.title}</p>}
109 </div>
110
111 {/* Short description */}
112 <div>
113 <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>
114 <textarea
115 value={form.short_description}
116 onChange={e => setField('short_description', e.target.value)}
117 placeholder="When the last dragon awakens..."
118 rows={3}
119 maxLength={280}
120 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'}`}
121 />
122 <div className="flex justify-between items-center mt-1">
123 {errors.short_description ? <p className="text-rose-400 text-xs">{errors.short_description}</p> : <span />}
124 <span className="text-slate-600 text-xs">{form.short_description.length}/280</span>
125 </div>
126 </div>
127
128 {/* Content */}
129 <div>
130 <label className="block text-sm font-medium text-slate-300 mb-1.5">Story Content *</label>
131 <textarea
132 value={form.content}
133 onChange={e => setField('content', e.target.value)}
134 placeholder="Begin your story here. This is the introduction that readers will see..."
135 rows={8}
136 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'}`}
137 />
138 {errors.content && <p className="text-rose-400 text-xs mt-1">{errors.content}</p>}
139 </div>
140
141 {/* Genres */}
142 <div>
143 <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>
144 <div className="flex flex-wrap gap-2 mb-2">
145 {ALL_GENRES.map(g => (
146 <button
147 key={g}
148 type="button"
149 onClick={() => toggleGenre(g)}
150 className={`px-3 py-1.5 rounded-full text-sm border transition-all ${
151 form.genres.includes(g)
152 ? 'bg-indigo-500/30 text-indigo-300 border-indigo-500/50'
153 : 'bg-slate-800 text-slate-400 border-slate-700 hover:border-slate-500'
154 }`}
155 >
156 {form.genres.includes(g) && <span className="mr-1">✓</span>}
157 {g}
158 </button>
159 ))}
160 </div>
161 {form.genres.length > 0 && (
162 <div className="flex flex-wrap gap-1 mt-1">
163 {form.genres.map(g => (
164 <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">
165 {g}
166 <button onClick={() => toggleGenre(g)}><X size={10} /></button>
167 </span>
168 ))}
169 </div>
170 )}
171 {errors.genres && <p className="text-rose-400 text-xs mt-1">{errors.genres}</p>}
172 </div>
173
174 {/* Mature content */}
175 <div className="flex items-center gap-3">
176 <button
177 type="button"
178 onClick={() => setField('mature_content', !form.mature_content)}
179 className={`w-11 h-6 rounded-full transition-colors ${form.mature_content ? 'bg-rose-500' : 'bg-slate-600'} relative flex-shrink-0`}
180 >
181 <div className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${form.mature_content ? 'translate-x-6' : 'translate-x-1'}`} />
182 </button>
183 <div>
184 <p className="text-white text-sm font-medium">Mature Content (18+)</p>
185 <p className="text-slate-500 text-xs">Enable for stories with adult themes</p>
186 </div>
187 </div>
188
189 {/* Actions */}
190 <div className="flex gap-3 pt-4 border-t border-slate-700">
191 <Button variant="secondary" className="flex-1" onClick={() => handleSubmit('draft')} loading={loading}>
192 Save as Draft
193 </Button>
194 <Button className="flex-1" onClick={() => handleSubmit('published')} loading={loading}>
195 <Feather size={16} />
196 Publish Story
197 </Button>
198 </div>
199 </div>
200
201 {/* AI Suggestions Sidebar */}
202 <div className="lg:col-span-1">
203 <StoryCreationAIPanel
204 ref={aiPanelRef}
205 title={form.title}
206 description={form.short_description}
207 content={form.content}
208 genres={form.genres}
209 />
210 </div>
211 </div>
212 </div>
213 )
214}
Note: See TracBrowser for help on using the repository browser.