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

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

Fixed user profile and reading lists

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