Changeset acf690c for chapterx-frontend


Ignore:
Timestamp:
03/22/26 17:58:40 (4 months ago)
Author:
kikisrbinoska <srbinoskakristina07@…>
Branches:
main
Children:
73b69b2
Parents:
b62cefc
Message:

Added fixes for the login,stories management and reading lists

Location:
chapterx-frontend/src
Files:
1 added
8 edited

Legend:

Unmodified
Added
Removed
  • chapterx-frontend/src/App.tsx

    rb62cefc racf690c  
    1 import React, { Suspense, lazy, ReactNode } from 'react'
     1import React, { Suspense, lazy, ReactNode, useEffect } from 'react'
    22import { Routes, Route, Navigate } from 'react-router-dom'
    33import { useAuthStore } from './store/authStore'
    44import { useUIStore } from './store/uiStore'
     5import { useStoryStore } from './store/storyStore'
    56import { UserRole } from './types'
    67import { Navbar } from './components/layout/Navbar'
     
    6768
    6869function App() {
     70  const { fetchStories, fetchChapters, fetchReadingLists } = useStoryStore()
     71
     72  useEffect(() => {
     73    fetchStories()
     74    fetchChapters()
     75    fetchReadingLists()
     76  }, [])
     77
    6978  return (
    7079    <div className="flex flex-col min-h-screen">
  • chapterx-frontend/src/pages/reading-list/ReadingListPage.tsx

    rb62cefc racf690c  
    1 import React, { useState } from 'react'
     1import React, { useState, useEffect } from 'react'
    22import { useNavigate } from 'react-router-dom'
    33import { Plus, BookOpen, Lock, Globe, Trash2, X } from 'lucide-react'
     
    1212  const navigate = useNavigate()
    1313  const { currentUser } = useAuthStore()
    14   const { readingLists, createReadingList, deleteReadingList, removeStoryFromList } = useStoryStore()
     14  const { readingLists, fetchReadingLists, createReadingList, deleteReadingList, removeStoryFromList } = useStoryStore()
     15
     16  useEffect(() => {
     17    fetchReadingLists()
     18  }, [])
    1519  const { addToast } = useUIStore()
    1620  const [createOpen, setCreateOpen] = useState(false)
     
    3135  const myLists = readingLists.filter(l => l.user_id === currentUser.user_id)
    3236
    33   const handleCreate = () => {
     37  const handleCreate = async () => {
    3438    if (!newListName.trim()) return
    35     createReadingList({
    36       list_id: Date.now(),
    37       user_id: currentUser.user_id,
    38       username: currentUser.username,
    39       name: newListName.trim(),
    40       description: newListDesc.trim() || undefined,
    41       is_public: isPublic,
    42       created_at: new Date().toISOString(),
    43       stories: [],
    44     })
    45     addToast(`"${newListName}" created!`)
     39    try {
     40      await createReadingList({
     41        list_id: Date.now(),
     42        user_id: currentUser.user_id,
     43        username: currentUser.username,
     44        name: newListName.trim(),
     45        description: newListDesc.trim() || undefined,
     46        is_public: isPublic,
     47        created_at: new Date().toISOString(),
     48        stories: [],
     49      })
     50      addToast(`"${newListName}" created!`)
     51    } catch (err: any) {
     52      addToast(err?.response?.data?.message || 'Failed to create list.', 'error')
     53      return
     54    }
    4655    setNewListName('')
    4756    setNewListDesc('')
     
    100109                  </div>
    101110                  <button
    102                     onClick={() => { deleteReadingList(list.list_id); addToast('List deleted', 'info') }}
     111                    onClick={async () => { try { await deleteReadingList(list.list_id); addToast('List deleted', 'info') } catch { addToast('Failed to delete list.', 'error') } }}
    103112                    className="text-slate-500 hover:text-rose-400 transition-colors p-1"
    104113                  >
  • chapterx-frontend/src/pages/story/StoryDetailPage.tsx

    rb62cefc racf690c  
    3939  const myLists = currentUser ? readingLists.filter(l => l.user_id === currentUser.user_id) : []
    4040
    41   const handleAddToList = (listId: number) => {
     41  const handleAddToList = async (listId: number) => {
    4242    const list = readingLists.find(l => l.list_id === listId)
    4343    if (!list) return
     
    5555      genres: story.genres,
    5656    }
    57     addStoryToList(listId, item)
    58     addToast(`Added to "${list.name}"!`)
     57    try {
     58      await addStoryToList(listId, item)
     59      addToast(`Added to "${list.name}"!`)
     60    } catch (err: any) {
     61      const msg = err?.response?.data?.message || ''
     62      if (msg.includes('already') || msg.includes('duplicate') || err?.response?.status === 400) {
     63        addToast('Already in this list', 'info')
     64      } else {
     65        addToast('Failed to add to list.', 'error')
     66      }
     67    }
    5968    setListModalOpen(false)
    6069  }
    6170
    62   const handleCreateList = () => {
     71  const handleCreateList = async () => {
    6372    if (!newListName.trim() || !currentUser) return
    6473    const newList = {
     
    6978      is_public: false,
    7079      created_at: new Date().toISOString(),
    71       stories: [{
     80      stories: [],
     81    }
     82    try {
     83      const realListId = await createReadingList(newList)
     84      await addStoryToList(realListId, {
    7285        item_id: Date.now() + 1,
    73         list_id: Date.now(),
     86        list_id: realListId,
    7487        story_id: story.story_id,
    7588        story_title: story.title,
     
    7790        added_at: new Date().toISOString(),
    7891        genres: story.genres,
    79       }],
    80     }
    81     createReadingList(newList)
    82     addToast(`Created "${newListName}" and added story!`)
     92      })
     93      addToast(`Created "${newListName}" and added story!`)
     94    } catch {
     95      addToast('Failed to create list.', 'error')
     96    }
    8397    setNewListName('')
    8498    setListModalOpen(false)
  • chapterx-frontend/src/pages/writer/CreateChapterPage.tsx

    rb62cefc racf690c  
    5656      is_published: isPublished,
    5757    }
    58     addChapter(chapter)
     58    try {
     59      await addChapter(chapter)
     60    } catch (err: any) {
     61      const msg = err?.response?.data?.message || err?.message || 'Failed to save chapter.'
     62      addToast(msg, 'error')
     63      setLoading(false)
     64      return
     65    }
    5966    addToast(isPublished ? 'Chapter published!' : 'Chapter saved as draft!')
    60     navigate(`/writer/edit-story/${storyId}`)
     67    navigate(isPublished ? `/story/${storyId}` : `/writer/edit-story/${storyId}`)
    6168    setLoading(false)
    6269  }
  • chapterx-frontend/src/pages/writer/CreateStoryPage.tsx

    rb62cefc racf690c  
    1 import React, { useState } from 'react'
     1import React, { useState, useRef } from 'react'
    22import { useNavigate } from 'react-router-dom'
    33import { Feather, ArrowLeft, X } from 'lucide-react'
     
    66import { useUIStore } from '../../store/uiStore'
    77import { Button } from '../../components/ui/Button'
     8import { StoryCreationAIPanel, StoryCreationAIPanelRef } from '../../components/writer/StoryCreationAIPanel'
    89import { Story } from '../../types'
    910
     
    1314  const navigate = useNavigate()
    1415  const { currentUser } = useAuthStore()
    15   const { addStory } = useStoryStore()
     16  const { addStory, addSuggestion } = useStoryStore()
    1617  const { addToast } = useUIStore()
     18  const aiPanelRef = useRef<StoryCreationAIPanelRef>(null)
    1719  const [form, setForm] = useState({
    1820    title: '',
     
    5355      total_views: 0,
    5456    }
    55     addStory(story)
     57    let realId: number
     58    try {
     59      realId = await addStory(story)
     60    } catch (err: any) {
     61      const msg = err?.response?.data?.message || err?.message || 'Failed to save story.'
     62      addToast(msg, 'error')
     63      setLoading(false)
     64      return
     65    }
     66
    5667    addToast(status === 'published' ? 'Story published!' : 'Draft saved!')
    57     navigate(`/writer/edit-story/${story.story_id}`)
     68    navigate(`/writer/edit-story/${realId}`)
    5869    setLoading(false)
    5970  }
     
    7384
    7485  return (
    75     <div className="max-w-3xl mx-auto px-4 py-8">
     86    <div className="max-w-6xl mx-auto px-4 py-8">
    7687      <div className="flex items-center gap-3 mb-8">
    7788        <button onClick={() => navigate('/writer')} className="text-slate-400 hover:text-white transition-colors">
     
    8495      </div>
    8596
    86       <div className="space-y-6">
    87         {/* Title */}
    88         <div>
    89           <label className="block text-sm font-medium text-slate-300 mb-1.5">Story Title *</label>
    90           <input
    91             value={form.title}
    92             onChange={e => setField('title', e.target.value)}
    93             placeholder="The Chronicles of Eldoria..."
    94             className={`w-full px-4 py-3 bg-slate-800 border rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 text-lg font-serif ${errors.title ? 'border-rose-500' : 'border-slate-700'}`}
    95           />
    96           {errors.title && <p className="text-rose-400 text-xs mt-1">{errors.title}</p>}
    97         </div>
    98 
    99         {/* Short description */}
    100         <div>
    101           <label className="block text-sm font-medium text-slate-300 mb-1.5">Short Description * <span className="text-slate-500 font-normal">(shown on story cards)</span></label>
    102           <textarea
    103             value={form.short_description}
    104             onChange={e => setField('short_description', e.target.value)}
    105             placeholder="When the last dragon awakens..."
    106             rows={3}
    107             maxLength={280}
    108             className={`w-full px-4 py-3 bg-slate-800 border rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 resize-none ${errors.short_description ? 'border-rose-500' : 'border-slate-700'}`}
    109           />
    110           <div className="flex justify-between items-center mt-1">
    111             {errors.short_description ? <p className="text-rose-400 text-xs">{errors.short_description}</p> : <span />}
    112             <span className="text-slate-600 text-xs">{form.short_description.length}/280</span>
    113           </div>
    114         </div>
    115 
    116         {/* Content */}
    117         <div>
    118           <label className="block text-sm font-medium text-slate-300 mb-1.5">Story Content *</label>
    119           <textarea
    120             value={form.content}
    121             onChange={e => setField('content', e.target.value)}
    122             placeholder="Begin your story here. This is the introduction that readers will see..."
    123             rows={8}
    124             className={`w-full px-4 py-3 bg-slate-800 border rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 resize-y font-serif ${errors.content ? 'border-rose-500' : 'border-slate-700'}`}
    125           />
    126           {errors.content && <p className="text-rose-400 text-xs mt-1">{errors.content}</p>}
    127         </div>
    128 
    129         {/* Genres */}
    130         <div>
    131           <label className="block text-sm font-medium text-slate-300 mb-2">Genres * <span className="text-slate-500 font-normal">(select 1-3)</span></label>
    132           <div className="flex flex-wrap gap-2 mb-2">
    133             {ALL_GENRES.map(g => (
    134               <button
    135                 key={g}
    136                 type="button"
    137                 onClick={() => toggleGenre(g)}
    138                 className={`px-3 py-1.5 rounded-full text-sm border transition-all ${
    139                   form.genres.includes(g)
    140                     ? 'bg-indigo-500/30 text-indigo-300 border-indigo-500/50'
    141                     : 'bg-slate-800 text-slate-400 border-slate-700 hover:border-slate-500'
    142                 }`}
    143               >
    144                 {form.genres.includes(g) && <span className="mr-1">✓</span>}
    145                 {g}
    146               </button>
    147             ))}
    148           </div>
    149           {form.genres.length > 0 && (
    150             <div className="flex flex-wrap gap-1 mt-1">
    151               {form.genres.map(g => (
    152                 <span key={g} className="flex items-center gap-1 text-xs px-2 py-0.5 bg-indigo-500/20 text-indigo-300 rounded-full">
     97      <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
     98        <div className="lg:col-span-2 space-y-6">
     99          {/* Title */}
     100          <div>
     101            <label className="block text-sm font-medium text-slate-300 mb-1.5">Story Title *</label>
     102            <input
     103              value={form.title}
     104              onChange={e => setField('title', e.target.value)}
     105              placeholder="The Chronicles of Eldoria..."
     106              className={`w-full px-4 py-3 bg-slate-800 border rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 text-lg font-serif ${errors.title ? 'border-rose-500' : 'border-slate-700'}`}
     107            />
     108            {errors.title && <p className="text-rose-400 text-xs mt-1">{errors.title}</p>}
     109          </div>
     110
     111          {/* Short description */}
     112          <div>
     113            <label className="block text-sm font-medium text-slate-300 mb-1.5">Short Description * <span className="text-slate-500 font-normal">(shown on story cards)</span></label>
     114            <textarea
     115              value={form.short_description}
     116              onChange={e => setField('short_description', e.target.value)}
     117              placeholder="When the last dragon awakens..."
     118              rows={3}
     119              maxLength={280}
     120              className={`w-full px-4 py-3 bg-slate-800 border rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 resize-none ${errors.short_description ? 'border-rose-500' : 'border-slate-700'}`}
     121            />
     122            <div className="flex justify-between items-center mt-1">
     123              {errors.short_description ? <p className="text-rose-400 text-xs">{errors.short_description}</p> : <span />}
     124              <span className="text-slate-600 text-xs">{form.short_description.length}/280</span>
     125            </div>
     126          </div>
     127
     128          {/* Content */}
     129          <div>
     130            <label className="block text-sm font-medium text-slate-300 mb-1.5">Story Content *</label>
     131            <textarea
     132              value={form.content}
     133              onChange={e => setField('content', e.target.value)}
     134              placeholder="Begin your story here. This is the introduction that readers will see..."
     135              rows={8}
     136              className={`w-full px-4 py-3 bg-slate-800 border rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 resize-y font-serif ${errors.content ? 'border-rose-500' : 'border-slate-700'}`}
     137            />
     138            {errors.content && <p className="text-rose-400 text-xs mt-1">{errors.content}</p>}
     139          </div>
     140
     141          {/* Genres */}
     142          <div>
     143            <label className="block text-sm font-medium text-slate-300 mb-2">Genres * <span className="text-slate-500 font-normal">(select 1-3)</span></label>
     144            <div className="flex flex-wrap gap-2 mb-2">
     145              {ALL_GENRES.map(g => (
     146                <button
     147                  key={g}
     148                  type="button"
     149                  onClick={() => toggleGenre(g)}
     150                  className={`px-3 py-1.5 rounded-full text-sm border transition-all ${
     151                    form.genres.includes(g)
     152                      ? 'bg-indigo-500/30 text-indigo-300 border-indigo-500/50'
     153                      : 'bg-slate-800 text-slate-400 border-slate-700 hover:border-slate-500'
     154                  }`}
     155                >
     156                  {form.genres.includes(g) && <span className="mr-1">✓</span>}
    153157                  {g}
    154                   <button onClick={() => toggleGenre(g)}><X size={10} /></button>
    155                 </span>
     158                </button>
    156159              ))}
    157160            </div>
    158           )}
    159           {errors.genres && <p className="text-rose-400 text-xs mt-1">{errors.genres}</p>}
     161            {form.genres.length > 0 && (
     162              <div className="flex flex-wrap gap-1 mt-1">
     163                {form.genres.map(g => (
     164                  <span key={g} className="flex items-center gap-1 text-xs px-2 py-0.5 bg-indigo-500/20 text-indigo-300 rounded-full">
     165                    {g}
     166                    <button onClick={() => toggleGenre(g)}><X size={10} /></button>
     167                  </span>
     168                ))}
     169              </div>
     170            )}
     171            {errors.genres && <p className="text-rose-400 text-xs mt-1">{errors.genres}</p>}
     172          </div>
     173
     174          {/* Mature content */}
     175          <div className="flex items-center gap-3">
     176            <button
     177              type="button"
     178              onClick={() => setField('mature_content', !form.mature_content)}
     179              className={`w-11 h-6 rounded-full transition-colors ${form.mature_content ? 'bg-rose-500' : 'bg-slate-600'} relative flex-shrink-0`}
     180            >
     181              <div className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${form.mature_content ? 'translate-x-6' : 'translate-x-1'}`} />
     182            </button>
     183            <div>
     184              <p className="text-white text-sm font-medium">Mature Content (18+)</p>
     185              <p className="text-slate-500 text-xs">Enable for stories with adult themes</p>
     186            </div>
     187          </div>
     188
     189          {/* Actions */}
     190          <div className="flex gap-3 pt-4 border-t border-slate-700">
     191            <Button variant="secondary" className="flex-1" onClick={() => handleSubmit('draft')} loading={loading}>
     192              Save as Draft
     193            </Button>
     194            <Button className="flex-1" onClick={() => handleSubmit('published')} loading={loading}>
     195              <Feather size={16} />
     196              Publish Story
     197            </Button>
     198          </div>
    160199        </div>
    161200
    162         {/* Mature content */}
    163         <div className="flex items-center gap-3">
    164           <button
    165             type="button"
    166             onClick={() => setField('mature_content', !form.mature_content)}
    167             className={`w-11 h-6 rounded-full transition-colors ${form.mature_content ? 'bg-rose-500' : 'bg-slate-600'} relative flex-shrink-0`}
    168           >
    169             <div className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${form.mature_content ? 'translate-x-6' : 'translate-x-1'}`} />
    170           </button>
    171           <div>
    172             <p className="text-white text-sm font-medium">Mature Content (18+)</p>
    173             <p className="text-slate-500 text-xs">Enable for stories with adult themes</p>
    174           </div>
    175         </div>
    176 
    177         {/* Actions */}
    178         <div className="flex gap-3 pt-4 border-t border-slate-700">
    179           <Button variant="secondary" className="flex-1" onClick={() => handleSubmit('draft')} loading={loading}>
    180             Save as Draft
    181           </Button>
    182           <Button className="flex-1" onClick={() => handleSubmit('published')} loading={loading}>
    183             <Feather size={16} />
    184             Publish Story
    185           </Button>
     201        {/* AI Suggestions Sidebar */}
     202        <div className="lg:col-span-1">
     203          <StoryCreationAIPanel
     204            ref={aiPanelRef}
     205            title={form.title}
     206            description={form.short_description}
     207            content={form.content}
     208            genres={form.genres}
     209          />
    186210        </div>
    187211      </div>
  • chapterx-frontend/src/pages/writer/EditStoryPage.tsx

    rb62cefc racf690c  
    6363    addToast('Story updated!')
    6464    setSaving(false)
     65    navigate(`/story/${story.story_id}`)
    6566  }
    6667
  • chapterx-frontend/src/store/authStore.ts

    rb62cefc racf690c  
    2727      token: null,
    2828      showMatureContent: true,
    29       allUsers: [...mockUsers],
     29      allUsers: [],
    3030
    3131      login: async (emailOrUsername, password) => {
     
    3636            : get().allUsers.find(u => u.username === emailOrUsername)?.email || emailOrUsername
    3737          const res = await axios.post(`${API_BASE}/auth/login`, { email, password }, { timeout: 3000 })
    38           const { token, userId, username } = res.data
    39           const user = get().allUsers.find(u => u.user_id === userId) ||
    40             get().allUsers.find(u => u.username === username) || {
    41               user_id: userId,
    42               username,
    43               email,
    44               name: username,
    45               surname: '',
    46               role: 'regular' as const,
    47               created_at: new Date().toISOString(),
    48               follower_count: 0,
    49               following_count: 0,
    50             }
     38          const { token, userId, username, name, surname, role } = res.data
     39          const user: User = {
     40            user_id: userId,
     41            username,
     42            email,
     43            name: name ?? username,
     44            surname: surname ?? '',
     45            role: role ?? 'regular',
     46            created_at: new Date().toISOString(),
     47            follower_count: 0,
     48            following_count: 0,
     49          }
    5150          set({ currentUser: user, token })
    5251          return
    53         } catch {
    54           // Fall through to mock login
     52        } catch (err: any) {
     53          // Only fall through to mock if the backend is unreachable (network/timeout)
     54          // If the backend responded with an error (4xx/5xx), surface it to the user
     55          if (err?.response) {
     56            const message = err.response.data?.message || err.response.data || 'Invalid email or password.'
     57            throw new Error(typeof message === 'string' ? message : 'Invalid email or password.')
     58          }
     59          // Network error / timeout — fall through to mock login
    5560        }
    5661
     
    6671
    6772      register: async (data, role) => {
    68         // Try backend first
    6973        try {
    7074          await axios.post(`${API_BASE}/auth/register`, data, { timeout: 3000 })
    71         } catch {
    72           // Fall through to mock register
     75          // Register doesn't return a token — auto-login to get one
     76          const loginRes = await axios.post(`${API_BASE}/auth/login`, { email: data.email, password: data.password }, { timeout: 3000 })
     77          const { token, userId, username } = loginRes.data
     78          const newUser: User = {
     79            user_id: userId,
     80            username,
     81            email: data.email,
     82            name: data.name,
     83            surname: data.surname,
     84            role,
     85            created_at: new Date().toISOString(),
     86            follower_count: 0,
     87            following_count: 0,
     88          }
     89          set(state => ({ allUsers: [...state.allUsers, newUser], currentUser: newUser, token }))
     90          return
     91        } catch (err: any) {
     92          if (err?.response) {
     93            const message = err.response.data?.message || err.response.data || 'Registration failed.'
     94            throw new Error(typeof message === 'string' ? message : 'Registration failed.')
     95          }
     96          // Network error / timeout — fall through to mock register
     97          console.warn('Backend unreachable during register, using mock:', err?.message)
    7398        }
    7499
  • chapterx-frontend/src/store/storyStore.ts

    rb62cefc racf690c  
    2323} from '../data/mockData'
    2424
     25const API = 'https://localhost:7125/api'
     26
     27function getAuthHeaders() {
     28  try {
     29    const token = JSON.parse(localStorage.getItem('chapterx-auth') || '{}')?.state?.token
     30    return token ? { Authorization: `Bearer ${token}` } : {}
     31  } catch {
     32    return {}
     33  }
     34}
     35
    2536interface LikeRecord {
    2637  userId: number
     
    3849  likedStories: LikeRecord[]
    3950
     51  // Fetch from backend
     52  fetchStories: () => Promise<void>
     53  fetchChapters: () => Promise<void>
     54  fetchReadingLists: () => Promise<void>
     55
    4056  // Story actions
    41   addStory: (story: Story) => void
    42   updateStory: (id: number, partial: Partial<Story>) => void
    43   deleteStory: (id: number) => void
     57  addStory: (story: Story) => Promise<number>
     58  updateStory: (id: number, partial: Partial<Story>) => Promise<void>
     59  deleteStory: (id: number) => Promise<void>
    4460  updateStoryStatus: (id: number, status: StoryStatus) => void
    4561
    4662  // Chapter actions
    47   addChapter: (chapter: Chapter) => void
    48   updateChapter: (id: number, partial: Partial<Chapter>) => void
    49   deleteChapter: (id: number) => void
     63  addChapter: (chapter: Chapter) => Promise<void>
     64  updateChapter: (id: number, partial: Partial<Chapter>) => Promise<void>
     65  deleteChapter: (id: number) => Promise<void>
    5066  incrementViewCount: (chapterId: number) => void
    5167
     
    7490
    7591  // Reading list actions
    76   createReadingList: (list: ReadingList) => void
    77   addStoryToList: (listId: number, item: ReadingListItem) => void
    78   removeStoryFromList: (listId: number, storyId: number) => void
    79   deleteReadingList: (listId: number) => void
     92  createReadingList: (list: ReadingList) => Promise<number>
     93  addStoryToList: (listId: number, item: ReadingListItem) => Promise<void>
     94  removeStoryFromList: (listId: number, storyId: number) => Promise<void>
     95  deleteReadingList: (listId: number) => Promise<void>
    8096}
    8197
     
    90106  likedStories: [],
    91107
    92   addStory: (story) =>
    93     set(state => ({ stories: [...state.stories, story] })),
    94 
    95   updateStory: (id, partial) =>
     108  fetchStories: async () => {
     109    try {
     110      const res = await axios.get(`${API}/stories`)
     111      const data: any[] = res.data?.stories ?? res.data ?? []
     112      const stories: Story[] = data.map((s: any) => ({
     113        story_id: s.id,
     114        user_id: s.userId,
     115        title: s.shortDescription,
     116        short_description: s.shortDescription,
     117        content: s.content,
     118        mature_content: s.matureContent,
     119        status: 'published' as StoryStatus,
     120        author_username: '',
     121        created_at: s.createdAt,
     122        updated_at: s.updatedAt,
     123        total_likes: 0,
     124        total_comments: 0,
     125        total_chapters: 0,
     126        total_views: 0,
     127        genres: [],
     128      }))
     129      if (stories.length > 0) set({ stories })
     130    } catch {
     131      // keep mock data on failure
     132    }
     133  },
     134
     135  fetchChapters: async () => {
     136    try {
     137      const res = await axios.get(`${API}/chapters`)
     138      const data: any[] = res.data?.chapters ?? res.data ?? []
     139      const chapters: Chapter[] = data.map((c: any) => ({
     140        chapter_id: c.id,
     141        story_id: c.storyId,
     142        title: c.title ?? c.name,
     143        content: c.content,
     144        chapter_number: c.number,
     145        word_count: c.wordCount ?? 0,
     146        view_count: c.viewCount ?? 0,
     147        is_published: true,
     148        created_at: c.createdAt,
     149        updated_at: c.updatedAt,
     150      }))
     151      if (chapters.length > 0) set({ chapters })
     152    } catch {
     153      // keep mock data on failure
     154    }
     155  },
     156
     157  addStory: async (story) => {
     158    set(state => ({ stories: [...state.stories, story] }))
     159    const res = await axios.post(`${API}/stories`, {
     160      matureContent: story.mature_content,
     161      shortDescription: story.title || story.short_description,
     162      image: null,
     163      content: story.content,
     164      userId: story.user_id,
     165    }, { headers: getAuthHeaders() })
     166    const backendId = res.data?.id ?? res.data
     167    if (backendId && backendId !== story.story_id) {
     168      set(state => ({
     169        stories: state.stories.map(s =>
     170          s.story_id === story.story_id ? { ...s, story_id: backendId } : s
     171        ),
     172        chapters: state.chapters.map(c =>
     173          c.story_id === story.story_id ? { ...c, story_id: backendId } : c
     174        ),
     175      }))
     176      return backendId
     177    }
     178    return story.story_id
     179  },
     180
     181  updateStory: async (id, partial) => {
    96182    set(state => ({
    97183      stories: state.stories.map(s => (s.story_id === id ? { ...s, ...partial } : s)),
    98     })),
    99 
    100   deleteStory: (id) =>
     184    }))
     185    try {
     186      const story = get().stories.find(s => s.story_id === id)
     187      if (!story) return
     188      await axios.put(`${API}/stories/${id}`, {
     189        id,
     190        matureContent: partial.mature_content ?? story.mature_content,
     191        shortDescription: partial.title ?? partial.short_description ?? story.title ?? story.short_description,
     192        image: null,
     193        content: partial.content ?? story.content,
     194      }, { headers: getAuthHeaders() })
     195    } catch {
     196      // keep optimistic update on failure
     197    }
     198  },
     199
     200  deleteStory: async (id) => {
    101201    set(state => ({
    102202      stories: state.stories.filter(s => s.story_id !== id),
     
    104204      comments: state.comments.filter(c => c.story_id !== id),
    105205      collaborations: state.collaborations.filter(c => c.story_id !== id),
    106     })),
     206    }))
     207    try {
     208      await axios.delete(`${API}/stories/${id}`, { headers: getAuthHeaders() })
     209    } catch {
     210      // keep optimistic delete on failure
     211    }
     212  },
    107213
    108214  updateStoryStatus: (id, status) =>
     
    113219    })),
    114220
    115   addChapter: (chapter) =>
     221  addChapter: async (chapter) => {
    116222    set(state => ({
    117223      chapters: [...state.chapters, chapter],
     
    121227          : s
    122228      ),
    123     })),
    124 
    125   updateChapter: (id, partial) =>
     229    }))
     230    const res = await axios.post(`${API}/chapters`, {
     231      number: chapter.chapter_number,
     232      name: chapter.title,
     233      title: chapter.title,
     234      content: chapter.content,
     235      storyId: chapter.story_id,
     236    }, { headers: getAuthHeaders() })
     237    const backendId = res.data?.id ?? res.data
     238    if (backendId && backendId !== chapter.chapter_id) {
     239      set(state => ({
     240        chapters: state.chapters.map(c =>
     241          c.chapter_id === chapter.chapter_id ? { ...c, chapter_id: backendId } : c
     242        ),
     243      }))
     244    }
     245  },
     246
     247  updateChapter: async (id, partial) => {
    126248    set(state => ({
    127249      chapters: state.chapters.map(c =>
    128250        c.chapter_id === id ? { ...c, ...partial, updated_at: new Date().toISOString() } : c
    129251      ),
    130     })),
    131 
    132   deleteChapter: (id) =>
     252    }))
     253    try {
     254      const chapter = get().chapters.find(c => c.chapter_id === id)
     255      if (!chapter) return
     256      await axios.put(`${API}/chapters/${id}`, {
     257        id,
     258        number: partial.chapter_number ?? chapter.chapter_number,
     259        name: partial.title ?? chapter.title,
     260        title: partial.title ?? chapter.title,
     261        content: partial.content ?? chapter.content,
     262        wordCount: partial.word_count ?? chapter.word_count,
     263      }, { headers: getAuthHeaders() })
     264    } catch {
     265      // keep optimistic update on failure
     266    }
     267  },
     268
     269  deleteChapter: async (id) => {
    133270    set(state => {
    134271      const chapter = state.chapters.find(c => c.chapter_id === id)
     
    143280          : state.stories,
    144281      }
    145     }),
     282    })
     283    try {
     284      await axios.delete(`${API}/chapters/${id}`, { headers: getAuthHeaders() })
     285    } catch {
     286      // keep optimistic delete on failure
     287    }
     288  },
    146289
    147290  incrementViewCount: (chapterId) =>
     
    218361  fetchSuggestions: async () => {
    219362    try {
    220       const res = await axios.get('https://localhost:7125/api/aisuggestions')
     363      const res = await axios.get(`${API}/aisuggestions`)
    221364      const data = res.data.aiSuggestions ?? res.data
    222365      const mapped: AISuggestion[] = data.map((s: any) => ({
     
    248391    }))
    249392    try {
    250       await axios.put(`https://localhost:7125/api/aisuggestions/${id}`, {
     393      await axios.put(`${API}/aisuggestions/${id}`, {
    251394        id,
    252395        originalText: s.original_text,
     
    268411    }))
    269412    try {
    270       await axios.put(`https://localhost:7125/api/aisuggestions/${id}`, {
     413      await axios.put(`${API}/aisuggestions/${id}`, {
    271414        id,
    272415        originalText: s.original_text,
     
    284427    set(state => ({ aiSuggestions: [...state.aiSuggestions, { ...suggestion, suggestion_id: tempId }] }))
    285428    try {
    286       const res = await axios.post('https://localhost:7125/api/aisuggestions', {
     429      const res = await axios.post('${API}/aisuggestions', {
    287430        originalText: suggestion.original_text,
    288431        suggestedText: suggestion.suggested_text,
     
    306449    set(state => ({ genres: state.genres.filter(g => g.genre_id !== id) })),
    307450
    308   createReadingList: (list) =>
    309     set(state => ({ readingLists: [...state.readingLists, list] })),
    310 
    311   addStoryToList: (listId, item) =>
     451  fetchReadingLists: async () => {
     452    try {
     453      const [listsRes, storiesRes] = await Promise.all([
     454        axios.get(`${API}/readinglists`),
     455        axios.get(`${API}/stories`),
     456      ])
     457      const data: any[] = listsRes.data?.readingLists ?? listsRes.data ?? []
     458      const storiesData: any[] = storiesRes.data?.stories ?? storiesRes.data ?? []
     459      const lists: ReadingList[] = data.map((l: any) => ({
     460        list_id: l.id,
     461        user_id: l.userId,
     462        username: '',
     463        name: l.name,
     464        description: l.content ?? '',
     465        is_public: l.isPublic,
     466        created_at: l.createdAt,
     467        stories: (l.readingListItems ?? []).map((i: any) => {
     468          const story = storiesData.find((s: any) => s.id === i.storyId)
     469          return {
     470            item_id: i.id ?? 0,
     471            list_id: l.id,
     472            story_id: i.storyId,
     473            story_title: story?.title ?? story?.shortDescription ?? `Story #${i.storyId}`,
     474            author_username: story?.authorUsername ?? '',
     475            added_at: i.addedAt ?? new Date().toISOString(),
     476            genres: story?.genres ?? [],
     477          }
     478        }),
     479      }))
     480      if (lists.length > 0) set({ readingLists: lists })
     481    } catch {
     482      // keep mock data on failure
     483    }
     484  },
     485
     486  createReadingList: async (list) => {
     487    set(state => ({ readingLists: [...state.readingLists, list] }))
     488    const res = await axios.post(`${API}/readinglists`, {
     489      name: list.name,
     490      content: list.description ?? null,
     491      isPublic: list.is_public,
     492      userId: list.user_id,
     493    }, { headers: getAuthHeaders() })
     494    const backendId = res.data?.id ?? res.data
     495    if (backendId && backendId !== list.list_id) {
     496      set(state => ({
     497        readingLists: state.readingLists.map(l =>
     498          l.list_id === list.list_id ? { ...l, list_id: backendId } : l
     499        ),
     500      }))
     501      return backendId
     502    }
     503    return list.list_id
     504  },
     505
     506  addStoryToList: async (listId, item) => {
    312507    set(state => ({
    313508      readingLists: state.readingLists.map(l =>
    314         l.list_id === listId
    315           ? { ...l, stories: [...l.stories, item] }
    316           : l
    317       ),
    318     })),
    319 
    320   removeStoryFromList: (listId, storyId) =>
     509        l.list_id === listId ? { ...l, stories: [...l.stories, item] } : l
     510      ),
     511    }))
     512    await axios.post(`${API}/readinglistitems`, {
     513      readingListId: listId,
     514      storyId: item.story_id,
     515    }, { headers: getAuthHeaders() })
     516  },
     517
     518  removeStoryFromList: async (listId, storyId) => {
    321519    set(state => ({
    322520      readingLists: state.readingLists.map(l =>
     
    325523          : l
    326524      ),
    327     })),
    328 
    329   deleteReadingList: (listId) =>
     525    }))
     526    try {
     527      // find the item id from backend list items to delete
     528      const res = await axios.get(`${API}/readinglistitems`)
     529      const items: any[] = res.data?.readingListItems ?? res.data ?? []
     530      const item = items.find((i: any) => i.readingListId === listId && i.storyId === storyId)
     531      if (item) await axios.delete(`${API}/readinglistitems/${item.id}`, { headers: getAuthHeaders() })
     532    } catch {
     533      // optimistic update already applied
     534    }
     535  },
     536
     537  deleteReadingList: async (listId) => {
    330538    set(state => ({
    331539      readingLists: state.readingLists.filter(l => l.list_id !== listId),
    332     })),
     540    }))
     541    await axios.delete(`${API}/readinglists/${listId}`, { headers: getAuthHeaders() })
     542  },
    333543}))
Note: See TracChangeset for help on using the changeset viewer.