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

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

Added frontend and imporoved AI Suggestions service

  • Property mode set to 100644
File size: 8.0 KB
Line 
1import React, { useState } 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 { Story } from '../../types'
9
10const ALL_GENRES = ['Fantasy', 'Sci-Fi', 'Romance', 'Historical Fiction', 'Adventure', 'Thriller', 'Mystery', 'Horror', 'Contemporary', 'Poetry']
11
12export const CreateStoryPage: React.FC = () => {
13 const navigate = useNavigate()
14 const { currentUser } = useAuthStore()
15 const { addStory } = useStoryStore()
16 const { addToast } = useUIStore()
17 const [form, setForm] = useState({
18 title: '',
19 short_description: '',
20 content: '',
21 genres: [] as string[],
22 mature_content: false,
23 })
24 const [errors, setErrors] = useState<Record<string, string>>({})
25 const [loading, setLoading] = useState(false)
26
27 const validate = () => {
28 const e: Record<string, string> = {}
29 if (!form.title.trim()) e.title = 'Title is required'
30 if (!form.short_description.trim()) e.short_description = 'Description is required'
31 if (form.short_description.length > 280) e.short_description = 'Max 280 characters'
32 if (!form.content.trim()) e.content = 'Story content is required'
33 if (form.genres.length === 0) e.genres = 'Select at least one genre'
34 setErrors(e)
35 return Object.keys(e).length === 0
36 }
37
38 const handleSubmit = async (status: 'draft' | 'published') => {
39 if (!validate() || !currentUser) return
40 setLoading(true)
41 await new Promise(r => setTimeout(r, 500))
42 const story: Story = {
43 story_id: Date.now(),
44 ...form,
45 user_id: currentUser.user_id,
46 author_username: currentUser.username,
47 status,
48 created_at: new Date().toISOString(),
49 updated_at: new Date().toISOString(),
50 total_likes: 0,
51 total_comments: 0,
52 total_chapters: 0,
53 total_views: 0,
54 }
55 addStory(story)
56 addToast(status === 'published' ? 'Story published!' : 'Draft saved!')
57 navigate(`/writer/edit-story/${story.story_id}`)
58 setLoading(false)
59 }
60
61 const toggleGenre = (g: string) => {
62 setForm(f => ({
63 ...f,
64 genres: f.genres.includes(g) ? f.genres.filter(x => x !== g) : [...f.genres, g],
65 }))
66 setErrors(e => ({ ...e, genres: '' }))
67 }
68
69 const setField = (field: string, value: string | boolean) => {
70 setForm(f => ({ ...f, [field]: value }))
71 setErrors(e => ({ ...e, [field]: '' }))
72 }
73
74 return (
75 <div className="max-w-3xl mx-auto px-4 py-8">
76 <div className="flex items-center gap-3 mb-8">
77 <button onClick={() => navigate('/writer')} className="text-slate-400 hover:text-white transition-colors">
78 <ArrowLeft size={20} />
79 </button>
80 <div>
81 <h1 className="font-serif text-2xl font-bold text-white">Create New Story</h1>
82 <p className="text-slate-400 text-sm mt-0.5">Tell your story to the world</p>
83 </div>
84 </div>
85
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">
153 {g}
154 <button onClick={() => toggleGenre(g)}><X size={10} /></button>
155 </span>
156 ))}
157 </div>
158 )}
159 {errors.genres && <p className="text-rose-400 text-xs mt-1">{errors.genres}</p>}
160 </div>
161
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>
186 </div>
187 </div>
188 </div>
189 )
190}
Note: See TracBrowser for help on using the repository browser.