source: chapterx-frontend/src/store/storyStore.ts@ 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: 16.5 KB
Line 
1import { create } from 'zustand'
2import axios from 'axios'
3import {
4 Story,
5 Chapter,
6 Comment,
7 Collaboration,
8 AISuggestion,
9 Genre,
10 ReadingList,
11 ReadingListItem,
12 StoryStatus,
13 PermissionLevel,
14} from '../types'
15import {
16 mockStories,
17 mockChapters,
18 mockComments,
19 mockCollaborations,
20 mockAISuggestions,
21 mockGenres,
22 mockReadingLists,
23} from '../data/mockData'
24
25const API = 'https://localhost:7125/api'
26
27function getAuthHeaders() {
28 try {
29 const token = JSON.parse(localStorage.getItem('chapterx-auth') || '{}')?.state?.token
30 return token ? { Authorization: `Bearer ${token}` } : {}
31 } catch {
32 return {}
33 }
34}
35
36interface LikeRecord {
37 userId: number
38 storyId: number
39}
40
41interface StoryStore {
42 stories: Story[]
43 chapters: Chapter[]
44 comments: Comment[]
45 collaborations: Collaboration[]
46 aiSuggestions: AISuggestion[]
47 genres: Genre[]
48 readingLists: ReadingList[]
49 likedStories: LikeRecord[]
50
51 // Fetch from backend
52 fetchStories: () => Promise<void>
53 fetchChapters: () => Promise<void>
54 fetchReadingLists: () => Promise<void>
55
56 // Story actions
57 addStory: (story: Story) => Promise<number>
58 updateStory: (id: number, partial: Partial<Story>) => Promise<void>
59 deleteStory: (id: number) => Promise<void>
60 updateStoryStatus: (id: number, status: StoryStatus) => void
61
62 // Chapter actions
63 addChapter: (chapter: Chapter) => Promise<void>
64 updateChapter: (id: number, partial: Partial<Chapter>) => Promise<void>
65 deleteChapter: (id: number) => Promise<void>
66 incrementViewCount: (chapterId: number) => void
67
68 // Comment actions
69 addComment: (comment: Comment) => void
70 deleteComment: (id: number) => void
71
72 // Like actions
73 toggleLike: (userId: number, storyId: number) => void
74 isLiked: (userId: number, storyId: number) => boolean
75
76 // Collaboration actions
77 addCollaboration: (collab: Collaboration) => void
78 updateCollaborationPermission: (userId: number, storyId: number, level: PermissionLevel) => void
79 removeCollaboration: (userId: number, storyId: number) => void
80
81 // AI Suggestion actions
82 fetchSuggestions: () => Promise<void>
83 acceptSuggestion: (id: number) => Promise<void>
84 rejectSuggestion: (id: number) => Promise<void>
85 addSuggestion: (suggestion: Omit<AISuggestion, 'suggestion_id'>) => Promise<void>
86
87 // Genre actions
88 addGenre: (genre: Genre) => void
89 deleteGenre: (id: number) => void
90
91 // Reading list actions
92 createReadingList: (list: ReadingList) => Promise<number>
93 addStoryToList: (listId: number, item: ReadingListItem) => Promise<void>
94 removeStoryFromList: (listId: number, storyId: number) => Promise<void>
95 deleteReadingList: (listId: number) => Promise<void>
96}
97
98export const useStoryStore = create<StoryStore>((set, get) => ({
99 stories: [...mockStories],
100 chapters: [...mockChapters],
101 comments: [...mockComments],
102 collaborations: [...mockCollaborations],
103 aiSuggestions: [...mockAISuggestions],
104 genres: [...mockGenres],
105 readingLists: [...mockReadingLists],
106 likedStories: [],
107
108 fetchStories: async () => {
109 try {
110 const res = await axios.get(`${API}/stories`)
111 const data: any[] = res.data?.stories ?? res.data ?? []
112 const stories: Story[] = data.map((s: any) => ({
113 story_id: s.id,
114 user_id: s.userId,
115 title: s.shortDescription,
116 short_description: s.shortDescription,
117 content: s.content,
118 mature_content: s.matureContent,
119 status: 'published' as StoryStatus,
120 author_username: '',
121 created_at: s.createdAt,
122 updated_at: s.updatedAt,
123 total_likes: 0,
124 total_comments: 0,
125 total_chapters: 0,
126 total_views: 0,
127 genres: [],
128 }))
129 if (stories.length > 0) set({ stories })
130 } catch {
131 // keep mock data on failure
132 }
133 },
134
135 fetchChapters: async () => {
136 try {
137 const res = await axios.get(`${API}/chapters`)
138 const data: any[] = res.data?.chapters ?? res.data ?? []
139 const chapters: Chapter[] = data.map((c: any) => ({
140 chapter_id: c.id,
141 story_id: c.storyId,
142 title: c.title ?? c.name,
143 content: c.content,
144 chapter_number: c.number,
145 word_count: c.wordCount ?? 0,
146 view_count: c.viewCount ?? 0,
147 is_published: true,
148 created_at: c.createdAt,
149 updated_at: c.updatedAt,
150 }))
151 if (chapters.length > 0) set({ chapters })
152 } catch {
153 // keep mock data on failure
154 }
155 },
156
157 addStory: async (story) => {
158 set(state => ({ stories: [...state.stories, story] }))
159 const res = await axios.post(`${API}/stories`, {
160 matureContent: story.mature_content,
161 shortDescription: story.title || story.short_description,
162 image: null,
163 content: story.content,
164 userId: story.user_id,
165 }, { headers: getAuthHeaders() })
166 const backendId = res.data?.id ?? res.data
167 if (backendId && backendId !== story.story_id) {
168 set(state => ({
169 stories: state.stories.map(s =>
170 s.story_id === story.story_id ? { ...s, story_id: backendId } : s
171 ),
172 chapters: state.chapters.map(c =>
173 c.story_id === story.story_id ? { ...c, story_id: backendId } : c
174 ),
175 }))
176 return backendId
177 }
178 return story.story_id
179 },
180
181 updateStory: async (id, partial) => {
182 set(state => ({
183 stories: state.stories.map(s => (s.story_id === id ? { ...s, ...partial } : s)),
184 }))
185 try {
186 const story = get().stories.find(s => s.story_id === id)
187 if (!story) return
188 await axios.put(`${API}/stories/${id}`, {
189 id,
190 matureContent: partial.mature_content ?? story.mature_content,
191 shortDescription: partial.title ?? partial.short_description ?? story.title ?? story.short_description,
192 image: null,
193 content: partial.content ?? story.content,
194 }, { headers: getAuthHeaders() })
195 } catch {
196 // keep optimistic update on failure
197 }
198 },
199
200 deleteStory: async (id) => {
201 set(state => ({
202 stories: state.stories.filter(s => s.story_id !== id),
203 chapters: state.chapters.filter(c => c.story_id !== id),
204 comments: state.comments.filter(c => c.story_id !== id),
205 collaborations: state.collaborations.filter(c => c.story_id !== id),
206 }))
207 try {
208 await axios.delete(`${API}/stories/${id}`, { headers: getAuthHeaders() })
209 } catch {
210 // keep optimistic delete on failure
211 }
212 },
213
214 updateStoryStatus: (id, status) =>
215 set(state => ({
216 stories: state.stories.map(s =>
217 s.story_id === id ? { ...s, status, updated_at: new Date().toISOString() } : s
218 ),
219 })),
220
221 addChapter: async (chapter) => {
222 set(state => ({
223 chapters: [...state.chapters, chapter],
224 stories: state.stories.map(s =>
225 s.story_id === chapter.story_id
226 ? { ...s, total_chapters: s.total_chapters + 1 }
227 : s
228 ),
229 }))
230 const res = await axios.post(`${API}/chapters`, {
231 number: chapter.chapter_number,
232 name: chapter.title,
233 title: chapter.title,
234 content: chapter.content,
235 storyId: chapter.story_id,
236 }, { headers: getAuthHeaders() })
237 const backendId = res.data?.id ?? res.data
238 if (backendId && backendId !== chapter.chapter_id) {
239 set(state => ({
240 chapters: state.chapters.map(c =>
241 c.chapter_id === chapter.chapter_id ? { ...c, chapter_id: backendId } : c
242 ),
243 }))
244 }
245 },
246
247 updateChapter: async (id, partial) => {
248 set(state => ({
249 chapters: state.chapters.map(c =>
250 c.chapter_id === id ? { ...c, ...partial, updated_at: new Date().toISOString() } : c
251 ),
252 }))
253 try {
254 const chapter = get().chapters.find(c => c.chapter_id === id)
255 if (!chapter) return
256 await axios.put(`${API}/chapters/${id}`, {
257 id,
258 number: partial.chapter_number ?? chapter.chapter_number,
259 name: partial.title ?? chapter.title,
260 title: partial.title ?? chapter.title,
261 content: partial.content ?? chapter.content,
262 wordCount: partial.word_count ?? chapter.word_count,
263 }, { headers: getAuthHeaders() })
264 } catch {
265 // keep optimistic update on failure
266 }
267 },
268
269 deleteChapter: async (id) => {
270 set(state => {
271 const chapter = state.chapters.find(c => c.chapter_id === id)
272 return {
273 chapters: state.chapters.filter(c => c.chapter_id !== id),
274 stories: chapter
275 ? state.stories.map(s =>
276 s.story_id === chapter.story_id
277 ? { ...s, total_chapters: Math.max(0, s.total_chapters - 1) }
278 : s
279 )
280 : state.stories,
281 }
282 })
283 try {
284 await axios.delete(`${API}/chapters/${id}`, { headers: getAuthHeaders() })
285 } catch {
286 // keep optimistic delete on failure
287 }
288 },
289
290 incrementViewCount: (chapterId) =>
291 set(state => ({
292 chapters: state.chapters.map(c =>
293 c.chapter_id === chapterId ? { ...c, view_count: c.view_count + 1 } : c
294 ),
295 })),
296
297 addComment: (comment) =>
298 set(state => ({
299 comments: [...state.comments, comment],
300 stories: state.stories.map(s =>
301 s.story_id === comment.story_id
302 ? { ...s, total_comments: s.total_comments + 1 }
303 : s
304 ),
305 })),
306
307 deleteComment: (id) =>
308 set(state => {
309 const comment = state.comments.find(c => c.comment_id === id)
310 return {
311 comments: state.comments.filter(c => c.comment_id !== id),
312 stories: comment
313 ? state.stories.map(s =>
314 s.story_id === comment.story_id
315 ? { ...s, total_comments: Math.max(0, s.total_comments - 1) }
316 : s
317 )
318 : state.stories,
319 }
320 }),
321
322 toggleLike: (userId, storyId) =>
323 set(state => {
324 const exists = state.likedStories.some(
325 l => l.userId === userId && l.storyId === storyId
326 )
327 return {
328 likedStories: exists
329 ? state.likedStories.filter(l => !(l.userId === userId && l.storyId === storyId))
330 : [...state.likedStories, { userId, storyId }],
331 stories: state.stories.map(s =>
332 s.story_id === storyId
333 ? { ...s, total_likes: exists ? s.total_likes - 1 : s.total_likes + 1 }
334 : s
335 ),
336 }
337 }),
338
339 isLiked: (userId, storyId) =>
340 get().likedStories.some(l => l.userId === userId && l.storyId === storyId),
341
342 addCollaboration: (collab) =>
343 set(state => ({ collaborations: [...state.collaborations, collab] })),
344
345 updateCollaborationPermission: (userId, storyId, level) =>
346 set(state => ({
347 collaborations: state.collaborations.map(c =>
348 c.user_id === userId && c.story_id === storyId
349 ? { ...c, permission_level: level }
350 : c
351 ),
352 })),
353
354 removeCollaboration: (userId, storyId) =>
355 set(state => ({
356 collaborations: state.collaborations.filter(
357 c => !(c.user_id === userId && c.story_id === storyId)
358 ),
359 })),
360
361 fetchSuggestions: async () => {
362 try {
363 const res = await axios.get(`${API}/aisuggestions`)
364 const data = res.data.aiSuggestions ?? res.data
365 const mapped: AISuggestion[] = data.map((s: any) => ({
366 suggestion_id: s.id,
367 chapter_id: s.storyId,
368 story_id: s.storyId,
369 original_text: s.originalText,
370 suggested_text: s.suggestedText,
371 suggestion_type: 'style' as const,
372 accepted: s.accepted === true ? true : s.accepted === false ? false : null,
373 applied_at: s.appliedAt ?? undefined,
374 }))
375 set({ aiSuggestions: mapped })
376 } catch {
377 // keep mock data on failure
378 }
379 },
380
381 acceptSuggestion: async (id) => {
382 const s = get().aiSuggestions.find(s => s.suggestion_id === id)
383 if (!s) return
384 // optimistic update
385 set(state => ({
386 aiSuggestions: state.aiSuggestions.map(s =>
387 s.suggestion_id === id
388 ? { ...s, accepted: true, applied_at: new Date().toISOString() }
389 : s
390 ),
391 }))
392 try {
393 await axios.put(`${API}/aisuggestions/${id}`, {
394 id,
395 originalText: s.original_text,
396 suggestedText: s.suggested_text,
397 accepted: true,
398 })
399 } catch {
400 // keep optimistic update even if backend fails
401 }
402 },
403
404 rejectSuggestion: async (id) => {
405 const s = get().aiSuggestions.find(s => s.suggestion_id === id)
406 if (!s) return
407 set(state => ({
408 aiSuggestions: state.aiSuggestions.map(s =>
409 s.suggestion_id === id ? { ...s, accepted: false } : s
410 ),
411 }))
412 try {
413 await axios.put(`${API}/aisuggestions/${id}`, {
414 id,
415 originalText: s.original_text,
416 suggestedText: s.suggested_text,
417 accepted: false,
418 })
419 } catch {
420 // keep optimistic update even if backend fails
421 }
422 },
423
424 addSuggestion: async (suggestion) => {
425 // optimistic local add
426 const tempId = Date.now()
427 set(state => ({ aiSuggestions: [...state.aiSuggestions, { ...suggestion, suggestion_id: tempId }] }))
428 try {
429 const res = await axios.post('${API}/aisuggestions', {
430 originalText: suggestion.original_text,
431 suggestedText: suggestion.suggested_text,
432 storyId: suggestion.chapter_id,
433 })
434 const newId = res.data.id ?? tempId
435 set(state => ({
436 aiSuggestions: state.aiSuggestions.map(s =>
437 s.suggestion_id === tempId ? { ...s, suggestion_id: newId } : s
438 ),
439 }))
440 } catch {
441 // keep local suggestion on failure
442 }
443 },
444
445 addGenre: (genre) =>
446 set(state => ({ genres: [...state.genres, genre] })),
447
448 deleteGenre: (id) =>
449 set(state => ({ genres: state.genres.filter(g => g.genre_id !== id) })),
450
451 fetchReadingLists: async () => {
452 try {
453 const [listsRes, storiesRes] = await Promise.all([
454 axios.get(`${API}/readinglists`),
455 axios.get(`${API}/stories`),
456 ])
457 const data: any[] = listsRes.data?.readingLists ?? listsRes.data ?? []
458 const storiesData: any[] = storiesRes.data?.stories ?? storiesRes.data ?? []
459 const lists: ReadingList[] = data.map((l: any) => ({
460 list_id: l.id,
461 user_id: l.userId,
462 username: '',
463 name: l.name,
464 description: l.content ?? '',
465 is_public: l.isPublic,
466 created_at: l.createdAt,
467 stories: (l.readingListItems ?? []).map((i: any) => {
468 const story = storiesData.find((s: any) => s.id === i.storyId)
469 return {
470 item_id: i.id ?? 0,
471 list_id: l.id,
472 story_id: i.storyId,
473 story_title: story?.title ?? story?.shortDescription ?? `Story #${i.storyId}`,
474 author_username: story?.authorUsername ?? '',
475 added_at: i.addedAt ?? new Date().toISOString(),
476 genres: story?.genres ?? [],
477 }
478 }),
479 }))
480 if (lists.length > 0) set({ readingLists: lists })
481 } catch {
482 // keep mock data on failure
483 }
484 },
485
486 createReadingList: async (list) => {
487 set(state => ({ readingLists: [...state.readingLists, list] }))
488 const res = await axios.post(`${API}/readinglists`, {
489 name: list.name,
490 content: list.description ?? null,
491 isPublic: list.is_public,
492 userId: list.user_id,
493 }, { headers: getAuthHeaders() })
494 const backendId = res.data?.id ?? res.data
495 if (backendId && backendId !== list.list_id) {
496 set(state => ({
497 readingLists: state.readingLists.map(l =>
498 l.list_id === list.list_id ? { ...l, list_id: backendId } : l
499 ),
500 }))
501 return backendId
502 }
503 return list.list_id
504 },
505
506 addStoryToList: async (listId, item) => {
507 set(state => ({
508 readingLists: state.readingLists.map(l =>
509 l.list_id === listId ? { ...l, stories: [...l.stories, item] } : l
510 ),
511 }))
512 await axios.post(`${API}/readinglistitems`, {
513 readingListId: listId,
514 storyId: item.story_id,
515 }, { headers: getAuthHeaders() })
516 },
517
518 removeStoryFromList: async (listId, storyId) => {
519 set(state => ({
520 readingLists: state.readingLists.map(l =>
521 l.list_id === listId
522 ? { ...l, stories: l.stories.filter(s => s.story_id !== storyId) }
523 : l
524 ),
525 }))
526 try {
527 // find the item id from backend list items to delete
528 const res = await axios.get(`${API}/readinglistitems`)
529 const items: any[] = res.data?.readingListItems ?? res.data ?? []
530 const item = items.find((i: any) => i.readingListId === listId && i.storyId === storyId)
531 if (item) await axios.delete(`${API}/readinglistitems/${item.id}`, { headers: getAuthHeaders() })
532 } catch {
533 // optimistic update already applied
534 }
535 },
536
537 deleteReadingList: async (listId) => {
538 set(state => ({
539 readingLists: state.readingLists.filter(l => l.list_id !== listId),
540 }))
541 await axios.delete(`${API}/readinglists/${listId}`, { headers: getAuthHeaders() })
542 },
543}))
Note: See TracBrowser for help on using the repository browser.