Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
import { create } from 'zustand';
export interface AIStepLog {
stepNumber: number;
stepName: string;
functionName: string;
status: 'pending' | 'success' | 'error';
timestamp: Date;
message?: string;
error?: string;
duration?: number; // milliseconds
}
export interface AIRequestLog {
id: string;
timestamp: Date;
function: string; // e.g., 'autoClusterKeywords', 'autoGenerateIdeas', 'autoGenerateContent', 'autoGenerateImages'
endpoint: string;
request: {
method: string;
body?: any;
params?: any;
};
response?: {
status: number;
data?: any;
error?: string;
errorType?: string; // e.g., 'DATABASE_ERROR', 'VALIDATION_ERROR', 'PERMISSION_ERROR'
};
status: 'pending' | 'success' | 'error';
duration?: number; // milliseconds
requestSteps: AIStepLog[]; // Request steps (INIT, PREP, SAVE, DONE)
responseSteps: AIStepLog[]; // Response steps (AI_CALL, PARSE)
}
interface AIRequestLogsStore {
logs: AIRequestLog[];
addLog: (log: Omit<AIRequestLog, 'id' | 'timestamp' | 'requestSteps' | 'responseSteps'>) => string;
updateLog: (logId: string, updates: Partial<AIRequestLog>) => void;
addRequestStep: (logId: string, step: Omit<AIStepLog, 'timestamp'>) => void;
addResponseStep: (logId: string, step: Omit<AIStepLog, 'timestamp'>) => void;
clearLogs: () => void;
maxLogs: number;
}
export const useAIRequestLogsStore = create<AIRequestLogsStore>((set, get) => ({
logs: [],
maxLogs: 20, // Keep last 20 logs
addLog: (log) => {
const newLog: AIRequestLog = {
...log,
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date(),
requestSteps: [],
responseSteps: [],
};
set((state) => {
const updatedLogs = [newLog, ...state.logs].slice(0, state.maxLogs);
return { logs: updatedLogs };
});
// Return the log ID so callers can add steps
return newLog.id;
},
updateLog: (logId, updates) => {
set((state) => {
const updatedLogs = state.logs.map((log) => {
if (log.id === logId) {
return { ...log, ...updates };
}
return log;
});
return { logs: updatedLogs };
});
},
addRequestStep: (logId, step) => {
set((state) => {
const updatedLogs = state.logs.map((log) => {
if (log.id === logId) {
const stepWithTimestamp: AIStepLog = {
...step,
timestamp: new Date(),
};
return {
...log,
requestSteps: [...log.requestSteps, stepWithTimestamp],
};
}
return log;
});
return { logs: updatedLogs };
});
},
addResponseStep: (logId, step) => {
set((state) => {
const updatedLogs = state.logs.map((log) => {
if (log.id === logId) {
const stepWithTimestamp: AIStepLog = {
...step,
timestamp: new Date(),
};
return {
...log,
responseSteps: [...log.responseSteps, stepWithTimestamp],
};
}
return log;
});
return { logs: updatedLogs };
});
},
clearLogs: () => {
set({ logs: [] });
},
}));

View File

@@ -0,0 +1,253 @@
/**
* Authentication Store (Zustand)
* Manages authentication state, user, and account information
*/
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface User {
id: number;
email: string;
username: string;
role: string;
account?: {
id: number;
name: string;
slug: string;
credits: number;
status: string;
};
}
interface AuthState {
user: User | null;
token: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
loading: boolean;
// Actions
login: (email: string, password: string) => Promise<void>;
logout: () => void;
register: (data: any) => Promise<void>;
setUser: (user: User | null) => void;
setToken: (token: string | null) => void;
refreshToken: () => Promise<void>;
refreshUser: () => Promise<void>;
}
export const useAuthStore = create<AuthState>()(
persist<AuthState>(
(set, get) => ({
user: null,
token: null,
isAuthenticated: false,
loading: false, // Always start with loading false - will be set true only during login/register
login: async (email, password) => {
set({ loading: true });
try {
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
const response = await fetch(`${API_BASE_URL}/v1/auth/login/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.message || 'Login failed');
}
// Store user and JWT tokens
set({
user: data.user,
token: data.tokens?.access || null,
refreshToken: data.tokens?.refresh || null,
isAuthenticated: true,
loading: false
});
} catch (error: any) {
// ALWAYS reset loading on error - critical to prevent stuck state
set({ loading: false });
throw new Error(error.message || 'Login failed');
} finally {
// Extra safety: ensure loading is ALWAYS false after login attempt completes
// This handles edge cases like network timeouts, browser crashes, etc.
const current = get();
if (current.loading) {
set({ loading: false });
}
}
},
logout: () => {
set({ user: null, token: null, refreshToken: null, isAuthenticated: false, loading: false });
},
register: async (registerData) => {
set({ loading: true });
try {
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
const response = await fetch(`${API_BASE_URL}/v1/auth/register/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...registerData,
password_confirm: registerData.password, // Add password_confirm
}),
});
const data = await response.json();
if (!response.ok || !data.success) {
// Handle validation errors
const errorMessage = data.message ||
(data.errors && typeof data.errors === 'object'
? JSON.stringify(data.errors)
: data.errors) ||
'Registration failed';
throw new Error(errorMessage);
}
// Store user and JWT tokens
set({
user: data.user,
token: data.tokens?.access || null,
refreshToken: data.tokens?.refresh || null,
isAuthenticated: true,
loading: false
});
} catch (error: any) {
// ALWAYS reset loading on error - critical to prevent stuck state
set({ loading: false });
throw new Error(error.message || 'Registration failed');
} finally {
// Extra safety: ensure loading is ALWAYS false after register attempt completes
// This handles edge cases like network timeouts, browser crashes, etc.
const current = get();
if (current.loading) {
set({ loading: false });
}
}
},
setUser: (user) => {
set({ user, isAuthenticated: !!user });
},
setToken: (token) => {
set({ token });
},
refreshToken: async () => {
const state = get();
if (!state.refreshToken) {
throw new Error('No refresh token available');
}
try {
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
const response = await fetch(`${API_BASE_URL}/v1/auth/refresh/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refresh: state.refreshToken }),
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.message || 'Token refresh failed');
}
// Update access token
set({ token: data.access });
// Also refresh user data to get latest account/plan information
// This ensures account/plan changes are reflected immediately
try {
await get().refreshUser();
} catch (userRefreshError) {
// If user refresh fails, don't fail the token refresh
console.warn('Failed to refresh user data during token refresh:', userRefreshError);
}
} catch (error: any) {
// If refresh fails, logout user
set({ user: null, token: null, refreshToken: null, isAuthenticated: false });
throw new Error(error.message || 'Token refresh failed');
}
},
refreshUser: async () => {
const state = get();
if (!state.token && !state.isAuthenticated) {
throw new Error('Not authenticated');
}
try {
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
const token = state.token || getAuthToken();
const response = await fetch(`${API_BASE_URL}/v1/auth/me/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
},
credentials: 'include',
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.message || 'Failed to refresh user data');
}
// Update user data with latest from server
// This ensures account/plan changes are reflected immediately
set({ user: data.user });
} catch (error: any) {
// If refresh fails, don't logout - just log the error
// User might still be authenticated, just couldn't refresh data
console.warn('Failed to refresh user data:', error);
throw new Error(error.message || 'Failed to refresh user data');
}
},
}),
{
name: 'auth-storage',
// NEVER persist loading - it should always be false on app start
partialize: (state) => ({
user: state.user,
token: state.token,
refreshToken: state.refreshToken,
isAuthenticated: state.isAuthenticated,
// Explicitly exclude loading from persistence
}),
// On rehydration, ensure loading is always false
onRehydrateStorage: () => {
return (state, error) => {
// Always reset loading to false on rehydration - critical fix for stuck loading state
if (state) {
state.loading = false;
}
if (error) {
console.error('Auth store rehydration error:', error);
// If rehydration fails, ensure loading is false
if (state) {
state.loading = false;
}
}
};
},
}
)
);

View File

@@ -0,0 +1,60 @@
/**
* Billing Store (Zustand)
* Manages credit balance and usage tracking
*/
import { create } from 'zustand';
import {
fetchCreditBalance,
fetchUsageSummary,
CreditBalance,
UsageSummary,
} from '../services/api';
interface BillingState {
balance: CreditBalance | null;
usageSummary: UsageSummary | null;
loading: boolean;
error: string | null;
// Actions
loadBalance: () => Promise<void>;
loadUsageSummary: (startDate?: string, endDate?: string) => Promise<void>;
reset: () => void;
}
export const useBillingStore = create<BillingState>((set, get) => ({
balance: null,
usageSummary: null,
loading: false,
error: null,
loadBalance: async () => {
set({ loading: true, error: null });
try {
const balance = await fetchCreditBalance();
set({ balance, loading: false });
} catch (error: any) {
set({ error: error.message, loading: false });
}
},
loadUsageSummary: async (startDate?: string, endDate?: string) => {
set({ loading: true, error: null });
try {
const summary = await fetchUsageSummary(startDate, endDate);
set({ usageSummary: summary, loading: false });
} catch (error: any) {
set({ error: error.message, loading: false });
}
},
reset: () => {
set({
balance: null,
usageSummary: null,
loading: false,
error: null,
});
},
}));

View File

@@ -0,0 +1,20 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface PageSizeStore {
pageSize: number;
setPageSize: (size: number) => void;
}
export const usePageSizeStore = create<PageSizeStore>()(
persist(
(set) => ({
pageSize: 10,
setPageSize: (size: number) => set({ pageSize: size }),
}),
{
name: 'igny8-page-size',
}
)
);

View File

@@ -0,0 +1,110 @@
/**
* Planner Store (Zustand)
* State management for Planner module (Keywords, Clusters, Ideas)
*/
import { create } from 'zustand';
import { fetchKeywords, fetchClusters, Keyword, Cluster } from '../services/api';
interface PlannerState {
// Keywords
keywords: Keyword[];
keywordsLoading: boolean;
keywordsError: string | null;
// Clusters
clusters: Cluster[];
clustersLoading: boolean;
clustersError: string | null;
// Actions - Keywords
fetchKeywords: (filters?: any) => Promise<void>;
createKeyword: (data: any) => Promise<void>;
updateKeyword: (id: number, data: any) => Promise<void>;
deleteKeyword: (id: number) => Promise<void>;
// Actions - Clusters
fetchClusters: (filters?: any) => Promise<void>;
// Reset
reset: () => void;
}
const initialState = {
keywords: [],
keywordsLoading: false,
keywordsError: null,
clusters: [],
clustersLoading: false,
clustersError: null,
};
export const usePlannerStore = create<PlannerState>((set, get) => ({
...initialState,
fetchKeywords: async (filters = {}) => {
set({ keywordsLoading: true, keywordsError: null });
try {
const keywords = await fetchKeywords(filters);
set({ keywords, keywordsLoading: false });
} catch (error: any) {
set({
keywordsError: error.message || 'Failed to fetch keywords',
keywordsLoading: false
});
}
},
createKeyword: async (data) => {
try {
// TODO: Implement create keyword API call
// await createKeyword(data);
// Refresh keywords list
await get().fetchKeywords();
} catch (error: any) {
set({ keywordsError: error.message || 'Failed to create keyword' });
throw error;
}
},
updateKeyword: async (id, data) => {
try {
// TODO: Implement update keyword API call
// await updateKeyword(id, data);
// Refresh keywords list
await get().fetchKeywords();
} catch (error: any) {
set({ keywordsError: error.message || 'Failed to update keyword' });
throw error;
}
},
deleteKeyword: async (id) => {
try {
// TODO: Implement delete keyword API call
// await deleteKeyword(id);
// Refresh keywords list
await get().fetchKeywords();
} catch (error: any) {
set({ keywordsError: error.message || 'Failed to delete keyword' });
throw error;
}
},
fetchClusters: async (filters = {}) => {
set({ clustersLoading: true, clustersError: null });
try {
const clusters = await fetchClusters(filters);
set({ clusters, clustersLoading: false });
} catch (error: any) {
set({
clustersError: error.message || 'Failed to fetch clusters',
clustersLoading: false
});
}
},
reset: () => {
set(initialState);
},
}));

View File

@@ -0,0 +1,168 @@
/**
* Sector Store (Zustand)
* Manages the currently selected/active sector for filtering data globally
* Sectors are scoped under the selected site, so sector selection depends on site selection.
*/
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { fetchSiteSectors } from '../services/api';
export interface Sector {
id: number;
name: string;
slug: string;
site_id: number;
is_active: boolean;
industry_sector?: number | null; // IndustrySector ID (FK to IndustrySector template)
}
interface SectorState {
activeSector: Sector | null;
sectors: Sector[]; // List of sectors for the current site
loading: boolean;
error: string | null;
// Actions
setActiveSector: (sector: Sector | null) => void;
loadSectorsForSite: (siteId: number) => Promise<Sector[]>;
clearActiveSector: () => void;
}
export const useSectorStore = create<SectorState>()(
persist<SectorState>(
(set, get) => ({
activeSector: null,
sectors: [],
loading: false,
error: null,
setActiveSector: (sector) => {
set({ activeSector: sector, error: null });
// Ensure sector is persisted immediately
if (typeof window !== 'undefined') {
try {
const state = get();
localStorage.setItem('sector-storage', JSON.stringify({
state: { activeSector: sector },
version: 0
}));
} catch (e) {
console.warn('Failed to persist sector to localStorage:', e);
}
}
// Dispatch event for components to refresh data
window.dispatchEvent(new CustomEvent('sectorChanged', { detail: { sectorId: sector?.id, siteId: sector?.site_id } }));
},
loadSectorsForSite: async (siteId: number) => {
set({ loading: true, error: null });
try {
// Don't try to load sectors if siteId is invalid
if (!siteId) {
set({ sectors: [], loading: false });
return [];
}
const sectorsData = await fetchSiteSectors(siteId);
const sectors: Sector[] = sectorsData.map((s: any) => ({
id: s.id,
name: s.name,
slug: s.slug,
site_id: siteId,
is_active: s.is_active !== false,
industry_sector: s.industry_sector || null, // Include industry_sector from API
}));
// Store sectors list in state
set({ sectors, loading: false });
// Default to "All Sectors" (null) - don't auto-select first sector
// Only update if current sector belongs to a different site
const currentSector = get().activeSector;
if (currentSector && currentSector.site_id !== siteId) {
// Current sector belongs to different site, reset to null (All Sectors)
set({ activeSector: null });
// Persist immediately
if (typeof window !== 'undefined') {
try {
localStorage.setItem('sector-storage', JSON.stringify({
state: { activeSector: null },
version: 0
}));
} catch (e) {
console.warn('Failed to persist sector to localStorage:', e);
}
}
// Dispatch event
window.dispatchEvent(new CustomEvent('sectorChanged', { detail: { sectorId: null, siteId } }));
} else if (currentSector && currentSector.site_id === siteId) {
// Verify current sector still exists in the list
const sectorStillExists = sectors.find(s => s.id === currentSector.id);
if (!sectorStillExists) {
// Current sector was deleted, reset to null (All Sectors)
set({ activeSector: null });
// Persist and dispatch event
if (typeof window !== 'undefined') {
try {
localStorage.setItem('sector-storage', JSON.stringify({
state: { activeSector: null },
version: 0
}));
} catch (e) {
console.warn('Failed to persist sector to localStorage:', e);
}
}
window.dispatchEvent(new CustomEvent('sectorChanged', { detail: { sectorId: null, siteId } }));
}
}
// If no current sector, keep it as null (All Sectors) - default behavior
return sectors;
} catch (error: any) {
// If 403 or 404, the site might be inactive or inaccessible - clear sectors silently
const isAccessError = error.status === 403 || error.status === 404;
if (isAccessError) {
console.warn(`Cannot load sectors for site ${siteId}: site may be inactive or inaccessible`);
set({
sectors: [],
activeSector: null, // Clear active sector if site is inaccessible
loading: false,
error: null // Don't show error for access issues
});
return [];
}
set({
error: error.message || 'Failed to load sectors',
loading: false
});
return [];
}
},
clearActiveSector: () => {
set({ activeSector: null, sectors: [], error: null });
if (typeof window !== 'undefined') {
try {
localStorage.setItem('sector-storage', JSON.stringify({
state: { activeSector: null },
version: 0
}));
} catch (e) {
console.warn('Failed to persist sector to localStorage:', e);
}
}
},
}),
{
name: 'sector-storage',
partialize: (state) => ({
activeSector: state.activeSector,
}),
}
)
);

View File

@@ -0,0 +1,156 @@
/**
* Settings Store (Zustand)
* Manages account and module settings
*/
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import {
fetchAccountSettings,
fetchAccountSetting,
createAccountSetting,
updateAccountSetting,
deleteAccountSetting,
fetchModuleSettings,
createModuleSetting,
updateModuleSetting,
AccountSetting,
ModuleSetting,
} from '../services/api';
interface SettingsState {
accountSettings: Record<string, AccountSetting>;
moduleSettings: Record<string, Record<string, ModuleSetting>>;
loading: boolean;
error: string | null;
// Actions
loadAccountSettings: () => Promise<void>;
loadAccountSetting: (key: string) => Promise<void>;
updateAccountSetting: (key: string, value: any) => Promise<void>;
loadModuleSettings: (moduleName: string) => Promise<void>;
updateModuleSetting: (moduleName: string, key: string, value: any) => Promise<void>;
reset: () => void;
}
export const useSettingsStore = create<SettingsState>()(
persist<SettingsState>(
(set, get) => ({
accountSettings: {},
moduleSettings: {},
loading: false,
error: null,
loadAccountSettings: async () => {
set({ loading: true, error: null });
try {
const response = await fetchAccountSettings();
const settingsMap: Record<string, AccountSetting> = {};
response.results.forEach(setting => {
settingsMap[setting.key] = setting;
});
set({ accountSettings: settingsMap, loading: false });
} catch (error: any) {
set({ error: error.message, loading: false });
}
},
loadAccountSetting: async (key: string) => {
try {
const setting = await fetchAccountSetting(key);
set(state => ({
accountSettings: { ...state.accountSettings, [key]: setting }
}));
} catch (error: any) {
set({ error: error.message });
}
},
updateAccountSetting: async (key: string, value: any) => {
set({ loading: true, error: null });
try {
const existing = get().accountSettings[key];
let setting: AccountSetting;
if (existing) {
setting = await updateAccountSetting(key, { config: value });
} else {
setting = await createAccountSetting({ key, config: value });
}
set(state => ({
accountSettings: { ...state.accountSettings, [key]: setting },
loading: false
}));
} catch (error: any) {
set({ error: error.message, loading: false });
throw error;
}
},
loadModuleSettings: async (moduleName: string) => {
set({ loading: true, error: null });
try {
const settings = await fetchModuleSettings(moduleName);
const settingsMap: Record<string, ModuleSetting> = {};
settings.forEach(setting => {
settingsMap[setting.key] = setting;
});
set(state => ({
moduleSettings: {
...state.moduleSettings,
[moduleName]: settingsMap
},
loading: false
}));
} catch (error: any) {
set({ error: error.message, loading: false });
}
},
updateModuleSetting: async (moduleName: string, key: string, value: any) => {
set({ loading: true, error: null });
try {
const existing = get().moduleSettings[moduleName]?.[key];
let setting: ModuleSetting;
if (existing) {
setting = await updateModuleSetting(moduleName, key, { config: value });
} else {
setting = await createModuleSetting({ module_name: moduleName, key, config: value });
}
set(state => ({
moduleSettings: {
...state.moduleSettings,
[moduleName]: {
...(state.moduleSettings[moduleName] || {}),
[key]: setting
}
},
loading: false
}));
} catch (error: any) {
set({ error: error.message, loading: false });
throw error;
}
},
reset: () => {
set({
accountSettings: {},
moduleSettings: {},
loading: false,
error: null,
});
},
}),
{
name: 'settings-storage',
partialize: (state) => ({
accountSettings: state.accountSettings,
moduleSettings: state.moduleSettings,
}),
}
)
);

View File

@@ -0,0 +1,140 @@
/**
* Site Store (Zustand)
* Manages the currently selected/active site for filtering data globally
* Since Site → Sector → Keywords, and all workflows depend on Keywords/Sector,
* filtering by site automatically filters all downstream data.
*/
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { Site, fetchSites } from '../services/api';
interface SiteState {
activeSite: Site | null;
loading: boolean;
error: string | null;
// Actions
setActiveSite: (site: Site | null) => void;
loadActiveSite: () => Promise<void>;
refreshActiveSite: () => Promise<void>;
}
export const useSiteStore = create<SiteState>()(
persist<SiteState>(
(set, get) => ({
activeSite: null,
loading: false,
error: null,
setActiveSite: (site) => {
set({ activeSite: site, error: null });
// Ensure site is persisted immediately
if (typeof window !== 'undefined') {
try {
const state = get();
localStorage.setItem('site-storage', JSON.stringify({
state: { activeSite: site },
version: 0
}));
} catch (e) {
console.warn('Failed to persist site to localStorage:', e);
}
}
// Dispatch event for components to refresh data
window.dispatchEvent(new CustomEvent('siteChanged', { detail: { siteId: site?.id } }));
// Load sectors for the newly selected site (if sector store is available)
if (site && typeof window !== 'undefined') {
// Import dynamically to avoid circular dependency
import('./sectorStore').then(({ useSectorStore }) => {
const sectorStore = useSectorStore.getState();
sectorStore.loadSectorsForSite(site.id);
}).catch(() => {
// Sector store might not be available yet, that's okay
});
}
},
loadActiveSite: async () => {
set({ loading: true, error: null });
try {
const response = await fetchSites();
const allSites = response.results || [];
// Find the first active site (or the one that was previously selected)
const activeSites = allSites.filter(site => site.is_active);
const previousSite = get().activeSite;
let siteToSet: Site | null = null;
if (previousSite) {
// Check if previous site still exists and is accessible
const previousSiteExists = allSites.find(s => s.id === previousSite.id);
if (previousSiteExists && previousSiteExists.is_active) {
// Keep the previously selected site if it's still active
siteToSet = previousSiteExists;
} else if (previousSiteExists && !previousSiteExists.is_active) {
// Previous site exists but is inactive - clear it and find a new active site
siteToSet = activeSites.length > 0 ? activeSites[0] : null;
} else {
// Previous site no longer exists - find a new active site
siteToSet = activeSites.length > 0 ? activeSites[0] : null;
}
} else if (activeSites.length > 0) {
// No previous site - use the first active site
siteToSet = activeSites[0];
}
set({ activeSite: siteToSet, loading: false });
// Immediately persist to localStorage for getActiveSiteId() to access
if (siteToSet && typeof window !== 'undefined') {
try {
localStorage.setItem('site-storage', JSON.stringify({
state: { activeSite: siteToSet },
version: 0
}));
} catch (e) {
console.warn('Failed to persist site to localStorage:', e);
}
}
} catch (error: any) {
set({
error: error.message || 'Failed to load active site',
loading: false
});
}
},
refreshActiveSite: async () => {
const currentSite = get().activeSite;
if (!currentSite) {
await get().loadActiveSite();
return;
}
set({ loading: true, error: null });
try {
const response = await fetchSites();
const updatedSite = (response.results || []).find(s => s.id === currentSite.id);
if (updatedSite && updatedSite.is_active) {
set({ activeSite: updatedSite, loading: false });
} else {
// Current site is no longer active, load a new one
await get().loadActiveSite();
}
} catch (error: any) {
set({
error: error.message || 'Failed to refresh active site',
loading: false
});
}
},
}),
{
name: 'site-storage',
partialize: (state) => ({
activeSite: state.activeSite,
}),
}
)
);