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

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

Added functional collaboration between users

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