import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import type { DeleteReference, ConfirmDeleteDetails } from '../components/ConfirmDeleteModal'; // Storage key for persisted state const STORAGE_KEY = 'bds-app-state'; // Tab types export type TabType = 'post' | 'media' | 'settings' | 'tags' | 'chat' | 'import' | 'metadata-diff'; export interface Tab { type: TabType; id: string; isTransient: boolean; } export interface TabState { tabs: Tab[]; activeTabId: string | null; } // Types export interface ProjectData { id: string; name: string; slug: string; description?: string; dataPath?: string; isActive: boolean; createdAt: string; updatedAt: string; } export interface PostData { id: string; title: string; slug: string; excerpt?: string; content: string; status: 'draft' | 'published' | 'archived'; author?: string; createdAt: string; updatedAt: string; publishedAt?: string; tags: string[]; categories: string[]; } export interface MediaData { id: string; filename: string; originalName: string; mimeType: string; size: number; width?: number; height?: number; title?: string; alt?: string; caption?: string; author?: string; createdAt: string; updatedAt: string; tags: string[]; } export interface TaskProgress { taskId: string; status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; progress: number; message: string; startTime: string; endTime?: string; error?: string; } export interface ErrorDetails { message: string; title?: string; stack?: string; } // Re-export types from ConfirmDeleteModal for convenience export type { DeleteReference, ConfirmDeleteDetails }; export type EditorMode = 'wysiwyg' | 'markdown' | 'preview'; // App State Store interface AppState { // Projects projects: ProjectData[]; activeProject: ProjectData | null; // Tabs tabs: Tab[]; activeTabId: string | null; // UI State activeView: 'posts' | 'media' | 'settings' | 'tags' | 'chat' | 'import'; sidebarVisible: boolean; panelVisible: boolean; selectedPostId: string | null; selectedMediaId: string | null; preferredEditorMode: EditorMode; // Data posts: PostData[]; media: MediaData[]; tasks: TaskProgress[]; // Pagination hasMorePosts: boolean; totalPosts: number; // Track which posts have unsaved changes (by post ID) dirtyPosts: Set; // Error modal errorModal: ErrorDetails | null; // Confirm delete modal confirmDeleteModal: ConfirmDeleteDetails | null; // Loading states isLoading: boolean; error: string | null; // Project Actions setProjects: (projects: ProjectData[]) => void; setActiveProject: (project: ProjectData | null) => void; addProject: (project: ProjectData) => void; updateProject: (id: string, project: Partial) => void; removeProject: (id: string) => void; // Tab Actions openTab: (tab: { type: TabType; id: string; isTransient: boolean }) => void; closeTab: (id: string) => void; setActiveTab: (id: string) => void; pinTab: (id: string) => void; clearTabs: () => void; getTabState: () => TabState; restoreTabState: (state: TabState) => void; // Actions setActiveView: (view: 'posts' | 'media' | 'settings' | 'tags' | 'chat' | 'import') => void; toggleSidebar: () => void; togglePanel: () => void; setSelectedPost: (id: string | null) => void; setSelectedMedia: (id: string | null) => void; setPreferredEditorMode: (mode: EditorMode) => void; setPosts: (posts: PostData[], hasMore?: boolean, total?: number) => void; appendPosts: (posts: PostData[], hasMore: boolean) => void; addPost: (post: PostData) => void; updatePost: (id: string, post: Partial) => void; removePost: (id: string) => void; // Dirty tracking markDirty: (id: string) => void; markClean: (id: string) => void; isDirty: (id: string) => boolean; // Error modal actions showErrorModal: (error: ErrorDetails) => void; hideErrorModal: () => void; // Confirm delete modal actions showConfirmDeleteModal: (details: ConfirmDeleteDetails) => void; hideConfirmDeleteModal: () => void; setMedia: (media: MediaData[]) => void; addMedia: (media: MediaData) => void; updateMedia: (id: string, media: Partial) => void; removeMedia: (id: string) => void; setTasks: (tasks: TaskProgress[]) => void; updateTask: (taskId: string, task: Partial) => void; // Loading Actions setLoading: (loading: boolean) => void; setError: (error: string | null) => void; } export const useAppStore = create()( persist( (set, get) => ({ // Initial Project State projects: [], activeProject: null, // Initial Tabs State tabs: [], activeTabId: null, // Initial UI State activeView: 'posts', sidebarVisible: true, panelVisible: false, selectedPostId: null, selectedMediaId: null, preferredEditorMode: 'wysiwyg', // Initial Data posts: [], media: [], tasks: [], // Pagination hasMorePosts: false, totalPosts: 0, // Dirty posts tracking dirtyPosts: new Set(), // Error modal errorModal: null, // Confirm delete modal confirmDeleteModal: null, // Initial Loading State isLoading: false, error: null, // Project Actions setProjects: (projects) => set({ projects }), setActiveProject: (activeProject) => set({ activeProject }), addProject: (project) => set((state) => ({ projects: [...state.projects, project] })), updateProject: (id, updatedProject) => set((state) => ({ projects: state.projects.map((p) => (p.id === id ? { ...p, ...updatedProject } : p)), })), removeProject: (id) => set((state) => ({ projects: state.projects.filter((p) => p.id !== id), })), // Tab Actions openTab: ({ type, id, isTransient }) => set((state) => { const existingTabIndex = state.tabs.findIndex((t) => t.id === id && t.type === type); if (existingTabIndex >= 0) { // Tab already exists - if trying to pin (isTransient=false), update it if (!isTransient) { const updatedTabs = [...state.tabs]; updatedTabs[existingTabIndex] = { ...updatedTabs[existingTabIndex], isTransient: false }; return { tabs: updatedTabs, activeTabId: id }; } // Just switch to the existing tab return { activeTabId: id }; } // If opening as transient, replace existing transient tab of the same type if (isTransient) { const transientIndex = state.tabs.findIndex((t) => t.isTransient && t.type === type); if (transientIndex >= 0) { const updatedTabs = [...state.tabs]; updatedTabs[transientIndex] = { type, id, isTransient: true }; return { tabs: updatedTabs, activeTabId: id }; } } // Add new tab const newTab: Tab = { type, id, isTransient }; return { tabs: [...state.tabs, newTab], activeTabId: id }; }), closeTab: (id) => set((state) => { const tabIndex = state.tabs.findIndex((t) => t.id === id); if (tabIndex === -1) return state; const newTabs = state.tabs.filter((t) => t.id !== id); let newActiveTabId = state.activeTabId; // If closing the active tab, activate an adjacent tab if (state.activeTabId === id) { if (newTabs.length === 0) { newActiveTabId = null; } else if (tabIndex < newTabs.length) { // Activate the tab that moved into this position (next tab) newActiveTabId = newTabs[tabIndex].id; } else { // Activate the previous tab newActiveTabId = newTabs[newTabs.length - 1].id; } } return { tabs: newTabs, activeTabId: newActiveTabId }; }), setActiveTab: (id) => set((state) => { // Only set if the tab exists const tabExists = state.tabs.some((t) => t.id === id); if (!tabExists) return state; return { activeTabId: id }; }), pinTab: (id) => set((state) => ({ tabs: state.tabs.map((t) => (t.id === id ? { ...t, isTransient: false } : t)), })), clearTabs: () => set({ tabs: [], activeTabId: null }), getTabState: () => { const state = get(); return { tabs: state.tabs, activeTabId: state.activeTabId }; }, restoreTabState: (tabState) => set({ tabs: tabState.tabs, activeTabId: tabState.activeTabId }), // UI Actions setActiveView: (view) => set({ activeView: view }), toggleSidebar: () => set((state) => ({ sidebarVisible: !state.sidebarVisible })), togglePanel: () => set((state) => ({ panelVisible: !state.panelVisible })), setSelectedPost: (id) => set({ selectedPostId: id }), setSelectedMedia: (id) => set({ selectedMediaId: id }), setPreferredEditorMode: (mode) => set({ preferredEditorMode: mode }), // Post Actions setPosts: (posts, hasMore = false, total = 0) => set({ posts, hasMorePosts: hasMore, totalPosts: total }), appendPosts: (newPosts, hasMore) => set((state) => { const existingIds = new Set(state.posts.map(p => p.id)); const uniqueNewPosts = newPosts.filter(p => !existingIds.has(p.id)); return { posts: [...state.posts, ...uniqueNewPosts], hasMorePosts: hasMore, }; }), addPost: (post) => set((state) => { if (state.posts.some(p => p.id === post.id)) return state; return { posts: [post, ...state.posts], totalPosts: state.totalPosts + 1 }; }), updatePost: (id, updatedPost) => set((state) => ({ posts: state.posts.map((p) => (p.id === id ? { ...p, ...updatedPost } : p)), })), removePost: (id) => set((state) => { const newDirtyPosts = new Set(state.dirtyPosts); newDirtyPosts.delete(id); return { posts: state.posts.filter((p) => p.id !== id), dirtyPosts: newDirtyPosts, selectedPostId: state.selectedPostId === id ? null : state.selectedPostId, }; }), // Dirty tracking markDirty: (id) => set((state) => ({ dirtyPosts: new Set([...state.dirtyPosts, id]), })), markClean: (id) => set((state) => { const newDirtyPosts = new Set(state.dirtyPosts); newDirtyPosts.delete(id); return { dirtyPosts: newDirtyPosts }; }), isDirty: (id) => get().dirtyPosts.has(id), // Error modal actions showErrorModal: (error) => set({ errorModal: error }), hideErrorModal: () => set({ errorModal: null }), // Confirm delete modal actions showConfirmDeleteModal: (details) => set({ confirmDeleteModal: details }), hideConfirmDeleteModal: () => set({ confirmDeleteModal: null }), // Media Actions setMedia: (media) => set({ media }), addMedia: (media) => set((state) => ({ media: [...state.media, media] })), updateMedia: (id, updatedMedia) => set((state) => ({ media: state.media.map((m) => (m.id === id ? { ...m, ...updatedMedia } : m)), })), removeMedia: (id) => set((state) => ({ media: state.media.filter((m) => m.id !== id), selectedMediaId: state.selectedMediaId === id ? null : state.selectedMediaId, })), // Task Actions setTasks: (tasks) => set({ tasks }), updateTask: (taskId, task) => set((state) => { const exists = state.tasks.some((t) => t.taskId === taskId); if (exists) { return { tasks: state.tasks.map((t) => (t.taskId === taskId ? { ...t, ...task } : t)) }; } // Add new task if it doesn't exist yet return { tasks: [...state.tasks, { taskId, status: 'running', progress: 0, message: '', startTime: new Date().toISOString(), ...task } as TaskProgress] }; }), // Loading Actions setLoading: (isLoading) => set({ isLoading }), setError: (error) => set({ error }), }), { name: STORAGE_KEY, // Only persist UI state, not data (which is loaded from backend) partialize: (state) => ({ activeView: state.activeView, sidebarVisible: state.sidebarVisible, panelVisible: state.panelVisible, selectedPostId: state.selectedPostId, selectedMediaId: state.selectedMediaId, preferredEditorMode: state.preferredEditorMode, // Tabs are persisted here for now (project-specific persistence handled separately) tabs: state.tabs, activeTabId: state.activeTabId, // Convert Set to array for storage dirtyPosts: [...state.dirtyPosts], }), // Merge function to restore Set from array merge: (persisted, current) => { const persistedState = persisted as Partial & { dirtyPosts?: string[]; tabs?: Tab[] }; return { ...current, ...persistedState, tabs: persistedState.tabs || [], activeTabId: persistedState.activeTabId || null, dirtyPosts: new Set(persistedState.dirtyPosts || []), }; }, } ) );