source: chapterx-frontend/src/store/storyStore.ts@ a6e33d1

main
Last change on this file since a6e33d1 was a6e33d1, checked in by kikisrbinoska <srbinoskakristina07@…>, 3 days ago

AI suggestions fixed

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