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

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

Fixed reading lists,comments and likes

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