- Timestamp:
- 03/22/26 17:58:40 (4 months ago)
- Branches:
- main
- Children:
- 73b69b2
- Parents:
- b62cefc
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
chapterx-frontend/src/pages/writer/CreateStoryPage.tsx
rb62cefc racf690c 1 import React, { useState } from 'react'1 import React, { useState, useRef } from 'react' 2 2 import { useNavigate } from 'react-router-dom' 3 3 import { Feather, ArrowLeft, X } from 'lucide-react' … … 6 6 import { useUIStore } from '../../store/uiStore' 7 7 import { Button } from '../../components/ui/Button' 8 import { StoryCreationAIPanel, StoryCreationAIPanelRef } from '../../components/writer/StoryCreationAIPanel' 8 9 import { Story } from '../../types' 9 10 … … 13 14 const navigate = useNavigate() 14 15 const { currentUser } = useAuthStore() 15 const { addStory } = useStoryStore()16 const { addStory, addSuggestion } = useStoryStore() 16 17 const { addToast } = useUIStore() 18 const aiPanelRef = useRef<StoryCreationAIPanelRef>(null) 17 19 const [form, setForm] = useState({ 18 20 title: '', … … 53 55 total_views: 0, 54 56 } 55 addStory(story) 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 56 67 addToast(status === 'published' ? 'Story published!' : 'Draft saved!') 57 navigate(`/writer/edit-story/${ story.story_id}`)68 navigate(`/writer/edit-story/${realId}`) 58 69 setLoading(false) 59 70 } … … 73 84 74 85 return ( 75 <div className="max-w- 3xl mx-auto px-4 py-8">86 <div className="max-w-6xl mx-auto px-4 py-8"> 76 87 <div className="flex items-center gap-3 mb-8"> 77 88 <button onClick={() => navigate('/writer')} className="text-slate-400 hover:text-white transition-colors"> … … 84 95 </div> 85 96 86 <div className="space-y-6"> 87 {/* Title */} 88 <div> 89 <label className="block text-sm font-medium text-slate-300 mb-1.5">Story Title *</label> 90 <input 91 value={form.title} 92 onChange={e => setField('title', e.target.value)} 93 placeholder="The Chronicles of Eldoria..." 94 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'}`} 95 /> 96 {errors.title && <p className="text-rose-400 text-xs mt-1">{errors.title}</p>} 97 </div> 98 99 {/* Short description */} 100 <div> 101 <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> 102 <textarea 103 value={form.short_description} 104 onChange={e => setField('short_description', e.target.value)} 105 placeholder="When the last dragon awakens..." 106 rows={3} 107 maxLength={280} 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 resize-none ${errors.short_description ? 'border-rose-500' : 'border-slate-700'}`} 109 /> 110 <div className="flex justify-between items-center mt-1"> 111 {errors.short_description ? <p className="text-rose-400 text-xs">{errors.short_description}</p> : <span />} 112 <span className="text-slate-600 text-xs">{form.short_description.length}/280</span> 113 </div> 114 </div> 115 116 {/* Content */} 117 <div> 118 <label className="block text-sm font-medium text-slate-300 mb-1.5">Story Content *</label> 119 <textarea 120 value={form.content} 121 onChange={e => setField('content', e.target.value)} 122 placeholder="Begin your story here. This is the introduction that readers will see..." 123 rows={8} 124 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'}`} 125 /> 126 {errors.content && <p className="text-rose-400 text-xs mt-1">{errors.content}</p>} 127 </div> 128 129 {/* Genres */} 130 <div> 131 <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> 132 <div className="flex flex-wrap gap-2 mb-2"> 133 {ALL_GENRES.map(g => ( 134 <button 135 key={g} 136 type="button" 137 onClick={() => toggleGenre(g)} 138 className={`px-3 py-1.5 rounded-full text-sm border transition-all ${ 139 form.genres.includes(g) 140 ? 'bg-indigo-500/30 text-indigo-300 border-indigo-500/50' 141 : 'bg-slate-800 text-slate-400 border-slate-700 hover:border-slate-500' 142 }`} 143 > 144 {form.genres.includes(g) && <span className="mr-1">✓</span>} 145 {g} 146 </button> 147 ))} 148 </div> 149 {form.genres.length > 0 && ( 150 <div className="flex flex-wrap gap-1 mt-1"> 151 {form.genres.map(g => ( 152 <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"> 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>} 153 157 {g} 154 <button onClick={() => toggleGenre(g)}><X size={10} /></button> 155 </span> 158 </button> 156 159 ))} 157 160 </div> 158 )} 159 {errors.genres && <p className="text-rose-400 text-xs mt-1">{errors.genres}</p>} 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> 160 199 </div> 161 200 162 {/* Mature content */} 163 <div className="flex items-center gap-3"> 164 <button 165 type="button" 166 onClick={() => setField('mature_content', !form.mature_content)} 167 className={`w-11 h-6 rounded-full transition-colors ${form.mature_content ? 'bg-rose-500' : 'bg-slate-600'} relative flex-shrink-0`} 168 > 169 <div className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${form.mature_content ? 'translate-x-6' : 'translate-x-1'}`} /> 170 </button> 171 <div> 172 <p className="text-white text-sm font-medium">Mature Content (18+)</p> 173 <p className="text-slate-500 text-xs">Enable for stories with adult themes</p> 174 </div> 175 </div> 176 177 {/* Actions */} 178 <div className="flex gap-3 pt-4 border-t border-slate-700"> 179 <Button variant="secondary" className="flex-1" onClick={() => handleSubmit('draft')} loading={loading}> 180 Save as Draft 181 </Button> 182 <Button className="flex-1" onClick={() => handleSubmit('published')} loading={loading}> 183 <Feather size={16} /> 184 Publish Story 185 </Button> 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 /> 186 210 </div> 187 211 </div>
Note:
See TracChangeset
for help on using the changeset viewer.
