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

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

Fixed writer section and admin management

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