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

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

Added corrected entitef from the ER diagram and changes for the logic to be coresponding to the ddl

  • Property mode set to 100644
File size: 19.6 KB
RevLine 
[b62cefc]1import { create } from 'zustand'
2import axios from 'axios'
3import {
4 Story,
5 Chapter,
6 Comment,
7 Collaboration,
8 AISuggestion,
[a6e33d1]9 SuggestionType,
[b62cefc]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
[acf690c]26const API = 'https://localhost:7125/api'
27
[73b69b2]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
[acf690c]49function getAuthHeaders() {
50 try {
51 const token = JSON.parse(localStorage.getItem('chapterx-auth') || '{}')?.state?.token
[0b502c2]52 if (!token || token === 'mock-token') return {}
53 return { Authorization: `Bearer ${token}` }
[acf690c]54 } catch {
55 return {}
56 }
57}
58
[b62cefc]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
[acf690c]74 // Fetch from backend
75 fetchStories: () => Promise<void>
76 fetchChapters: () => Promise<void>
[7fbb91c]77 fetchCollaborations: () => Promise<void>
[acf690c]78 fetchReadingLists: () => Promise<void>
[73b69b2]79 fetchUserReadingLists: (userId: number) => Promise<void>
80 fetchGenres: () => Promise<void>
[acf690c]81
[b62cefc]82 // Story actions
[acf690c]83 addStory: (story: Story) => Promise<number>
84 updateStory: (id: number, partial: Partial<Story>) => Promise<void>
85 deleteStory: (id: number) => Promise<void>
[b62cefc]86 updateStoryStatus: (id: number, status: StoryStatus) => void
87
88 // Chapter actions
[acf690c]89 addChapter: (chapter: Chapter) => Promise<void>
90 updateChapter: (id: number, partial: Partial<Chapter>) => Promise<void>
91 deleteChapter: (id: number) => Promise<void>
[b62cefc]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
[7fbb91c]103 addCollaboration: (collab: Collaboration) => Promise<void>
[b62cefc]104 updateCollaborationPermission: (userId: number, storyId: number, level: PermissionLevel) => void
[7fbb91c]105 removeCollaboration: (userId: number, storyId: number) => Promise<void>
[b62cefc]106
107 // AI Suggestion actions
[a6e33d1]108 fetchSuggestions: (chapterId: number) => Promise<void>
[b62cefc]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
[73b69b2]114 addGenre: (name: string) => Promise<void>
115 deleteGenre: (id: number) => Promise<void>
[b62cefc]116
117 // Reading list actions
[acf690c]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>
[b62cefc]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],
[73b69b2]131 readingLists: [],
[b62cefc]132 likedStories: [],
133
[acf690c]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,
[99c1e45]141 title: s.title ?? '',
142 short_description: s.shortDescription ?? '',
143 content: s.content ?? '',
144 cover_image: s.image ?? undefined,
[acf690c]145 mature_content: s.matureContent,
[e882b92]146 status: (s.status ?? 'draft') as StoryStatus,
[73b69b2]147 author_username: s.writer?.user?.username ?? '',
[acf690c]148 created_at: s.createdAt,
149 updated_at: s.updatedAt,
[0b502c2]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,
[73b69b2]154 genres: (s.hasGenres ?? []).map((hg: any) => hg.genre?.name ?? hg.name).filter(Boolean),
[acf690c]155 }))
156 if (stories.length > 0) set({ stories })
157 } catch {
158 // keep mock data on failure
159 }
160 },
[b62cefc]161
[acf690c]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
[7fbb91c]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: '',
[e882b92]195 role: c.role as any,
196 permission_level: (c.permissionLevel ?? 3) as any,
[7fbb91c]197 joined_at: c.createdAt,
198 }))
199 set({ collaborations })
200 } catch {
201 // keep existing
202 }
203 },
204
[acf690c]205 addStory: async (story) => {
206 set(state => ({ stories: [...state.stories, story] }))
[99c1e45]207 const imageUrl = story.cover_image?.startsWith('http') ? story.cover_image : null
[acf690c]208 const res = await axios.post(`${API}/stories`, {
209 matureContent: story.mature_content,
[99c1e45]210 title: story.title,
211 shortDescription: story.short_description,
212 image: imageUrl,
[acf690c]213 content: story.content,
214 userId: story.user_id,
[73b69b2]215 genres: story.genres ?? [],
[acf690c]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) => {
[b62cefc]233 set(state => ({
234 stories: state.stories.map(s => (s.story_id === id ? { ...s, ...partial } : s)),
[acf690c]235 }))
236 try {
237 const story = get().stories.find(s => s.story_id === id)
238 if (!story) return
[99c1e45]239 const rawImage = partial.cover_image ?? story.cover_image ?? null
240 const imageUrl = rawImage?.startsWith('http') ? rawImage : null
[acf690c]241 await axios.put(`${API}/stories/${id}`, {
242 id,
243 matureContent: partial.mature_content ?? story.mature_content,
[99c1e45]244 title: partial.title ?? story.title,
245 shortDescription: partial.short_description ?? story.short_description,
246 image: imageUrl,
[acf690c]247 content: partial.content ?? story.content,
[e882b92]248 status: partial.status ?? story.status,
[acf690c]249 }, { headers: getAuthHeaders() })
250 } catch {
251 // keep optimistic update on failure
252 }
253 },
[b62cefc]254
[acf690c]255 deleteStory: async (id) => {
[b62cefc]256 set(state => ({
257 stories: state.stories.filter(s => s.story_id !== id),
258 chapters: state.chapters.filter(c => c.story_id !== id),
259 comments: state.comments.filter(c => c.story_id !== id),
260 collaborations: state.collaborations.filter(c => c.story_id !== id),
[acf690c]261 }))
262 try {
263 await axios.delete(`${API}/stories/${id}`, { headers: getAuthHeaders() })
264 } catch {
265 // keep optimistic delete on failure
266 }
267 },
[b62cefc]268
[e882b92]269 updateStoryStatus: (id, status) => {
[b62cefc]270 set(state => ({
271 stories: state.stories.map(s =>
272 s.story_id === id ? { ...s, status, updated_at: new Date().toISOString() } : s
273 ),
[e882b92]274 }))
275 get().updateStory(id, { status })
276 },
[b62cefc]277
[acf690c]278 addChapter: async (chapter) => {
[b62cefc]279 set(state => ({
280 chapters: [...state.chapters, chapter],
281 stories: state.stories.map(s =>
282 s.story_id === chapter.story_id
283 ? { ...s, total_chapters: s.total_chapters + 1 }
284 : s
285 ),
[acf690c]286 }))
287 const res = await axios.post(`${API}/chapters`, {
288 number: chapter.chapter_number,
289 name: chapter.title,
290 title: chapter.title,
291 content: chapter.content,
292 storyId: chapter.story_id,
293 }, { headers: getAuthHeaders() })
294 const backendId = res.data?.id ?? res.data
295 if (backendId && backendId !== chapter.chapter_id) {
296 set(state => ({
297 chapters: state.chapters.map(c =>
298 c.chapter_id === chapter.chapter_id ? { ...c, chapter_id: backendId } : c
299 ),
300 }))
301 }
302 },
[b62cefc]303
[acf690c]304 updateChapter: async (id, partial) => {
[b62cefc]305 set(state => ({
306 chapters: state.chapters.map(c =>
307 c.chapter_id === id ? { ...c, ...partial, updated_at: new Date().toISOString() } : c
308 ),
[acf690c]309 }))
310 try {
311 const chapter = get().chapters.find(c => c.chapter_id === id)
312 if (!chapter) return
313 await axios.put(`${API}/chapters/${id}`, {
314 id,
315 number: partial.chapter_number ?? chapter.chapter_number,
316 name: partial.title ?? chapter.title,
317 title: partial.title ?? chapter.title,
318 content: partial.content ?? chapter.content,
319 wordCount: partial.word_count ?? chapter.word_count,
320 }, { headers: getAuthHeaders() })
321 } catch {
322 // keep optimistic update on failure
323 }
324 },
[b62cefc]325
[acf690c]326 deleteChapter: async (id) => {
[b62cefc]327 set(state => {
328 const chapter = state.chapters.find(c => c.chapter_id === id)
329 return {
330 chapters: state.chapters.filter(c => c.chapter_id !== id),
331 stories: chapter
332 ? state.stories.map(s =>
333 s.story_id === chapter.story_id
334 ? { ...s, total_chapters: Math.max(0, s.total_chapters - 1) }
335 : s
336 )
337 : state.stories,
338 }
[acf690c]339 })
340 try {
341 await axios.delete(`${API}/chapters/${id}`, { headers: getAuthHeaders() })
342 } catch {
343 // keep optimistic delete on failure
344 }
345 },
[b62cefc]346
[99c1e45]347 incrementViewCount: (chapterId) => {
348 const chapter = get().chapters.find(c => c.chapter_id === chapterId)
[b62cefc]349 set(state => ({
350 chapters: state.chapters.map(c =>
351 c.chapter_id === chapterId ? { ...c, view_count: c.view_count + 1 } : c
352 ),
[99c1e45]353 stories: state.stories.map(s =>
354 s.story_id === chapter?.story_id ? { ...s, total_views: s.total_views + 1 } : s
355 ),
356 }))
357 axios.patch(`${API}/chapters/${chapterId}/view`, null, { headers: getAuthHeaders() }).catch(() => {})
358 },
[b62cefc]359
360 addComment: (comment) =>
361 set(state => ({
362 comments: [...state.comments, comment],
363 stories: state.stories.map(s =>
364 s.story_id === comment.story_id
365 ? { ...s, total_comments: s.total_comments + 1 }
366 : s
367 ),
368 })),
369
370 deleteComment: (id) =>
371 set(state => {
372 const comment = state.comments.find(c => c.comment_id === id)
373 return {
374 comments: state.comments.filter(c => c.comment_id !== id),
375 stories: comment
376 ? state.stories.map(s =>
377 s.story_id === comment.story_id
378 ? { ...s, total_comments: Math.max(0, s.total_comments - 1) }
379 : s
380 )
381 : state.stories,
382 }
383 }),
384
385 toggleLike: (userId, storyId) =>
386 set(state => {
387 const exists = state.likedStories.some(
388 l => l.userId === userId && l.storyId === storyId
389 )
390 return {
391 likedStories: exists
392 ? state.likedStories.filter(l => !(l.userId === userId && l.storyId === storyId))
393 : [...state.likedStories, { userId, storyId }],
394 stories: state.stories.map(s =>
395 s.story_id === storyId
396 ? { ...s, total_likes: exists ? s.total_likes - 1 : s.total_likes + 1 }
397 : s
398 ),
399 }
400 }),
401
402 isLiked: (userId, storyId) =>
403 get().likedStories.some(l => l.userId === userId && l.storyId === storyId),
404
[7fbb91c]405 addCollaboration: async (collab) => {
406 set(state => ({ collaborations: [...state.collaborations, collab] }))
407 try {
408 await axios.post(`${API}/collaborations`, {
409 userId: collab.user_id,
410 storyId: collab.story_id,
411 role: collab.role,
[e882b92]412 permissionLevel: collab.permission_level,
[7fbb91c]413 }, { headers: getAuthHeaders() })
414 } catch {
415 // keep optimistic
416 }
417 },
[b62cefc]418
419 updateCollaborationPermission: (userId, storyId, level) =>
420 set(state => ({
421 collaborations: state.collaborations.map(c =>
422 c.user_id === userId && c.story_id === storyId
423 ? { ...c, permission_level: level }
424 : c
425 ),
426 })),
427
[7fbb91c]428 removeCollaboration: async (userId, storyId) => {
[b62cefc]429 set(state => ({
430 collaborations: state.collaborations.filter(
431 c => !(c.user_id === userId && c.story_id === storyId)
432 ),
[7fbb91c]433 }))
434 try {
435 await axios.delete(`${API}/collaborations/user/${userId}/story/${storyId}`, { headers: getAuthHeaders() })
436 } catch {
437 // keep optimistic
438 }
439 },
[b62cefc]440
[a6e33d1]441 fetchSuggestions: async (chapterId) => {
[b62cefc]442 try {
[a6e33d1]443 const res = await axios.get(`${API}/aisuggestions/chapter/${chapterId}`)
444 const data: any[] = res.data ?? []
[b62cefc]445 const mapped: AISuggestion[] = data.map((s: any) => ({
446 suggestion_id: s.id,
[a6e33d1]447 chapter_id: chapterId,
[b62cefc]448 story_id: s.storyId,
449 original_text: s.originalText,
450 suggested_text: s.suggestedText,
[e882b92]451 suggestion_type: (s.suggestionType ?? 'style') as SuggestionType,
452 explanation: s.explanation ?? '',
[b62cefc]453 accepted: s.accepted === true ? true : s.accepted === false ? false : null,
[e882b92]454 created_at: s.createdAt ?? new Date().toISOString(),
[b62cefc]455 applied_at: s.appliedAt ?? undefined,
456 }))
457 set({ aiSuggestions: mapped })
458 } catch {
459 // keep mock data on failure
460 }
461 },
462
463 acceptSuggestion: async (id) => {
464 const s = get().aiSuggestions.find(s => s.suggestion_id === id)
465 if (!s) return
466 // optimistic update
467 set(state => ({
468 aiSuggestions: state.aiSuggestions.map(s =>
469 s.suggestion_id === id
470 ? { ...s, accepted: true, applied_at: new Date().toISOString() }
471 : s
472 ),
473 }))
474 try {
[acf690c]475 await axios.put(`${API}/aisuggestions/${id}`, {
[b62cefc]476 id,
477 originalText: s.original_text,
478 suggestedText: s.suggested_text,
479 accepted: true,
[a6e33d1]480 }, { headers: getAuthHeaders() })
[b62cefc]481 } catch {
482 // keep optimistic update even if backend fails
483 }
484 },
485
486 rejectSuggestion: async (id) => {
487 const s = get().aiSuggestions.find(s => s.suggestion_id === id)
488 if (!s) return
489 set(state => ({
490 aiSuggestions: state.aiSuggestions.map(s =>
491 s.suggestion_id === id ? { ...s, accepted: false } : s
492 ),
493 }))
494 try {
[acf690c]495 await axios.put(`${API}/aisuggestions/${id}`, {
[b62cefc]496 id,
497 originalText: s.original_text,
498 suggestedText: s.suggested_text,
499 accepted: false,
[a6e33d1]500 }, { headers: getAuthHeaders() })
[b62cefc]501 } catch {
502 // keep optimistic update even if backend fails
503 }
504 },
505
506 addSuggestion: async (suggestion) => {
507 // optimistic local add
508 const tempId = Date.now()
509 set(state => ({ aiSuggestions: [...state.aiSuggestions, { ...suggestion, suggestion_id: tempId }] }))
510 try {
[a6e33d1]511 const res = await axios.post(`${API}/aisuggestions`, {
[b62cefc]512 originalText: suggestion.original_text,
513 suggestedText: suggestion.suggested_text,
[e882b92]514 suggestionType: suggestion.suggestion_type,
[b62cefc]515 storyId: suggestion.chapter_id,
[a6e33d1]516 }, { headers: getAuthHeaders() })
[b62cefc]517 const newId = res.data.id ?? tempId
518 set(state => ({
519 aiSuggestions: state.aiSuggestions.map(s =>
520 s.suggestion_id === tempId ? { ...s, suggestion_id: newId } : s
521 ),
522 }))
523 } catch {
524 // keep local suggestion on failure
525 }
526 },
527
[73b69b2]528 fetchGenres: async () => {
529 try {
530 const res = await axios.get(`${API}/genres`)
531 const data: any[] = res.data?.genres ?? res.data ?? []
532 const genres: Genre[] = data.map((g: any) => ({ genre_id: g.id, name: g.name }))
533 if (genres.length > 0) set({ genres })
534 } catch {
535 // keep mock data on failure
536 }
537 },
[b62cefc]538
[73b69b2]539 addGenre: async (name) => {
540 const res = await axios.post(`${API}/genres`, { name }, { headers: getAuthHeaders() })
541 const id = res.data?.id ?? res.data
542 set(state => ({ genres: [...state.genres, { genre_id: id, name }] }))
543 },
544
545 deleteGenre: async (id) => {
546 set(state => ({ genres: state.genres.filter(g => g.genre_id !== id) }))
547 try {
548 await axios.delete(`${API}/genres/${id}`, { headers: getAuthHeaders() })
549 } catch {
550 // optimistic delete already applied
551 }
552 },
[b62cefc]553
[acf690c]554 fetchReadingLists: async () => {
555 try {
[73b69b2]556 const res = await axios.get(`${API}/readinglists`)
557 const data: any[] = res.data ?? []
558 const lists: ReadingList[] = data.map(mapReadingList)
559 set({ readingLists: lists })
[acf690c]560 } catch {
[73b69b2]561 // keep existing data on failure
562 }
563 },
564
565 fetchUserReadingLists: async (userId) => {
566 try {
567 const res = await axios.get(`${API}/readinglists/user/${userId}`, { headers: getAuthHeaders() })
568 const data: any[] = res.data ?? []
569 const lists: ReadingList[] = data.map(mapReadingList)
570 set(state => ({
571 readingLists: [
572 ...state.readingLists.filter(l => l.user_id !== userId),
573 ...lists,
574 ]
575 }))
576 } catch (err) {
577 console.error('fetchUserReadingLists failed:', err)
[acf690c]578 }
579 },
[b62cefc]580
[acf690c]581 createReadingList: async (list) => {
582 set(state => ({ readingLists: [...state.readingLists, list] }))
583 const res = await axios.post(`${API}/readinglists`, {
584 name: list.name,
585 content: list.description ?? null,
586 isPublic: list.is_public,
587 userId: list.user_id,
588 }, { headers: getAuthHeaders() })
589 const backendId = res.data?.id ?? res.data
590 if (backendId && backendId !== list.list_id) {
591 set(state => ({
592 readingLists: state.readingLists.map(l =>
593 l.list_id === list.list_id ? { ...l, list_id: backendId } : l
594 ),
595 }))
596 return backendId
597 }
598 return list.list_id
599 },
600
601 addStoryToList: async (listId, item) => {
[b62cefc]602 set(state => ({
603 readingLists: state.readingLists.map(l =>
[acf690c]604 l.list_id === listId ? { ...l, stories: [...l.stories, item] } : l
[b62cefc]605 ),
[acf690c]606 }))
607 await axios.post(`${API}/readinglistitems`, {
608 readingListId: listId,
609 storyId: item.story_id,
610 }, { headers: getAuthHeaders() })
611 },
[b62cefc]612
[acf690c]613 removeStoryFromList: async (listId, storyId) => {
[b62cefc]614 set(state => ({
615 readingLists: state.readingLists.map(l =>
616 l.list_id === listId
617 ? { ...l, stories: l.stories.filter(s => s.story_id !== storyId) }
618 : l
619 ),
[acf690c]620 }))
621 try {
[73b69b2]622 await axios.delete(`${API}/readinglistitems/${listId}/story/${storyId}`, { headers: getAuthHeaders() })
[acf690c]623 } catch {
624 // optimistic update already applied
625 }
626 },
[b62cefc]627
[acf690c]628 deleteReadingList: async (listId) => {
[b62cefc]629 set(state => ({
630 readingLists: state.readingLists.filter(l => l.list_id !== listId),
[acf690c]631 }))
632 await axios.delete(`${API}/readinglists/${listId}`, { headers: getAuthHeaders() })
633 },
[b62cefc]634}))
Note: See TracBrowser for help on using the repository browser.