Changeset acf690c for chapterx-frontend/src/pages
- Timestamp:
- 03/22/26 17:58:40 (4 months ago)
- Branches:
- main
- Children:
- 73b69b2
- Parents:
- b62cefc
- Location:
- chapterx-frontend/src/pages
- Files:
-
- 5 edited
-
reading-list/ReadingListPage.tsx (modified) (4 diffs)
-
story/StoryDetailPage.tsx (modified) (4 diffs)
-
writer/CreateChapterPage.tsx (modified) (1 diff)
-
writer/CreateStoryPage.tsx (modified) (6 diffs)
-
writer/EditStoryPage.tsx (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
chapterx-frontend/src/pages/reading-list/ReadingListPage.tsx
rb62cefc racf690c 1 import React, { useState } from 'react'1 import React, { useState, useEffect } from 'react' 2 2 import { useNavigate } from 'react-router-dom' 3 3 import { Plus, BookOpen, Lock, Globe, Trash2, X } from 'lucide-react' … … 12 12 const navigate = useNavigate() 13 13 const { currentUser } = useAuthStore() 14 const { readingLists, createReadingList, deleteReadingList, removeStoryFromList } = useStoryStore() 14 const { readingLists, fetchReadingLists, createReadingList, deleteReadingList, removeStoryFromList } = useStoryStore() 15 16 useEffect(() => { 17 fetchReadingLists() 18 }, []) 15 19 const { addToast } = useUIStore() 16 20 const [createOpen, setCreateOpen] = useState(false) … … 31 35 const myLists = readingLists.filter(l => l.user_id === currentUser.user_id) 32 36 33 const handleCreate = () => {37 const handleCreate = async () => { 34 38 if (!newListName.trim()) return 35 createReadingList({ 36 list_id: Date.now(), 37 user_id: currentUser.user_id, 38 username: currentUser.username, 39 name: newListName.trim(), 40 description: newListDesc.trim() || undefined, 41 is_public: isPublic, 42 created_at: new Date().toISOString(), 43 stories: [], 44 }) 45 addToast(`"${newListName}" created!`) 39 try { 40 await createReadingList({ 41 list_id: Date.now(), 42 user_id: currentUser.user_id, 43 username: currentUser.username, 44 name: newListName.trim(), 45 description: newListDesc.trim() || undefined, 46 is_public: isPublic, 47 created_at: new Date().toISOString(), 48 stories: [], 49 }) 50 addToast(`"${newListName}" created!`) 51 } catch (err: any) { 52 addToast(err?.response?.data?.message || 'Failed to create list.', 'error') 53 return 54 } 46 55 setNewListName('') 47 56 setNewListDesc('') … … 100 109 </div> 101 110 <button 102 onClick={ () => { deleteReadingList(list.list_id); addToast('List deleted', 'info')}}111 onClick={async () => { try { await deleteReadingList(list.list_id); addToast('List deleted', 'info') } catch { addToast('Failed to delete list.', 'error') } }} 103 112 className="text-slate-500 hover:text-rose-400 transition-colors p-1" 104 113 > -
chapterx-frontend/src/pages/story/StoryDetailPage.tsx
rb62cefc racf690c 39 39 const myLists = currentUser ? readingLists.filter(l => l.user_id === currentUser.user_id) : [] 40 40 41 const handleAddToList = (listId: number) => {41 const handleAddToList = async (listId: number) => { 42 42 const list = readingLists.find(l => l.list_id === listId) 43 43 if (!list) return … … 55 55 genres: story.genres, 56 56 } 57 addStoryToList(listId, item) 58 addToast(`Added to "${list.name}"!`) 57 try { 58 await addStoryToList(listId, item) 59 addToast(`Added to "${list.name}"!`) 60 } catch (err: any) { 61 const msg = err?.response?.data?.message || '' 62 if (msg.includes('already') || msg.includes('duplicate') || err?.response?.status === 400) { 63 addToast('Already in this list', 'info') 64 } else { 65 addToast('Failed to add to list.', 'error') 66 } 67 } 59 68 setListModalOpen(false) 60 69 } 61 70 62 const handleCreateList = () => {71 const handleCreateList = async () => { 63 72 if (!newListName.trim() || !currentUser) return 64 73 const newList = { … … 69 78 is_public: false, 70 79 created_at: new Date().toISOString(), 71 stories: [{ 80 stories: [], 81 } 82 try { 83 const realListId = await createReadingList(newList) 84 await addStoryToList(realListId, { 72 85 item_id: Date.now() + 1, 73 list_id: Date.now(),86 list_id: realListId, 74 87 story_id: story.story_id, 75 88 story_title: story.title, … … 77 90 added_at: new Date().toISOString(), 78 91 genres: story.genres, 79 }], 80 } 81 createReadingList(newList) 82 addToast(`Created "${newListName}" and added story!`) 92 }) 93 addToast(`Created "${newListName}" and added story!`) 94 } catch { 95 addToast('Failed to create list.', 'error') 96 } 83 97 setNewListName('') 84 98 setListModalOpen(false) -
chapterx-frontend/src/pages/writer/CreateChapterPage.tsx
rb62cefc racf690c 56 56 is_published: isPublished, 57 57 } 58 addChapter(chapter) 58 try { 59 await addChapter(chapter) 60 } catch (err: any) { 61 const msg = err?.response?.data?.message || err?.message || 'Failed to save chapter.' 62 addToast(msg, 'error') 63 setLoading(false) 64 return 65 } 59 66 addToast(isPublished ? 'Chapter published!' : 'Chapter saved as draft!') 60 navigate( `/writer/edit-story/${storyId}`)67 navigate(isPublished ? `/story/${storyId}` : `/writer/edit-story/${storyId}`) 61 68 setLoading(false) 62 69 } -
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> -
chapterx-frontend/src/pages/writer/EditStoryPage.tsx
rb62cefc racf690c 63 63 addToast('Story updated!') 64 64 setSaving(false) 65 navigate(`/story/${story.story_id}`) 65 66 } 66 67
Note:
See TracChangeset
for help on using the changeset viewer.
