- Add mc_api_call_machine() function for MC_MACHINE_TOKEN auth - Update mc_api_call() to use machine token when available - Allows cron jobs to authenticate without cookie-based login - No breaking changes - cookie auth still works for interactive use - Also updates default API URL to production (was localhost)
249 lines
6.8 KiB
TypeScript
249 lines
6.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { Document, Folder, DocumentType, DEFAULT_FOLDERS } from '@/types/documents';
|
|
|
|
const generateId = () => Math.random().toString(36).substring(2, 9);
|
|
|
|
// Transform Supabase row to Document type
|
|
const transformRowToDocument = (row: any): Document => ({
|
|
id: row.id,
|
|
title: row.title,
|
|
content: row.content,
|
|
type: row.type as DocumentType,
|
|
folder: row.folder || 'General',
|
|
tags: row.tags || [],
|
|
createdAt: row.created_at,
|
|
updatedAt: row.updated_at,
|
|
size: row.size || 0,
|
|
description: row.description,
|
|
});
|
|
|
|
const getInitialFolders = (): Folder[] =>
|
|
DEFAULT_FOLDERS.map((name) => ({
|
|
id: generateId(),
|
|
name,
|
|
count: 0,
|
|
}));
|
|
|
|
// Helper to fetch with timeout
|
|
async function fetchWithTimeout(url: string, options: RequestInit = {}, timeoutMs = 10000) {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
signal: controller.signal,
|
|
});
|
|
clearTimeout(timeoutId);
|
|
return response;
|
|
} catch (error) {
|
|
clearTimeout(timeoutId);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function useDocuments() {
|
|
const [documents, setDocuments] = useState<Document[]>([]);
|
|
const [folders, setFolders] = useState<Folder[]>([]);
|
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Load from API on mount
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
|
|
async function loadDocuments() {
|
|
try {
|
|
console.log('[useDocuments] Starting fetch...');
|
|
|
|
const response = await fetchWithTimeout('/api/documents', {
|
|
headers: {
|
|
'Cache-Control': 'no-cache',
|
|
},
|
|
cache: 'no-store',
|
|
}, 10000);
|
|
|
|
console.log('[useDocuments] Response status:', response.status);
|
|
|
|
// Try to parse JSON even if response is not ok (to get error details)
|
|
let result;
|
|
try {
|
|
result = await response.json();
|
|
} catch (parseErr) {
|
|
throw new Error(`Failed to parse response: ${response.statusText}`);
|
|
}
|
|
|
|
console.log('[useDocuments] API response:', result);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(result.error || `HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
if (!isMounted) return;
|
|
|
|
if (result.error) {
|
|
console.error('[useDocuments] API returned error:', result.error);
|
|
setError(result.error);
|
|
setDocuments([]);
|
|
} else if (result.documents && Array.isArray(result.documents)) {
|
|
console.log(`[useDocuments] Loaded ${result.documents.length} documents`);
|
|
setDocuments(result.documents.map(transformRowToDocument));
|
|
setError(null);
|
|
} else {
|
|
console.log('[useDocuments] No documents found or invalid format');
|
|
setDocuments([]);
|
|
setError(null);
|
|
}
|
|
} catch (err: any) {
|
|
console.error('[useDocuments] Exception:', err);
|
|
if (!isMounted) return;
|
|
|
|
if (err.name === 'AbortError') {
|
|
setError('Request timed out. Please check your connection and try again.');
|
|
} else {
|
|
setError(err.message || 'Failed to load documents');
|
|
}
|
|
setDocuments([]);
|
|
} finally {
|
|
if (isMounted) {
|
|
console.log('[useDocuments] Setting isLoaded to true');
|
|
setIsLoaded(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
loadDocuments();
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, []);
|
|
|
|
// Initialize folders (local only for now)
|
|
useEffect(() => {
|
|
setFolders(getInitialFolders());
|
|
}, []);
|
|
|
|
// Update folder counts whenever documents change
|
|
useEffect(() => {
|
|
setFolders((prev) =>
|
|
prev.map((folder) => ({
|
|
...folder,
|
|
count: documents.filter((d) => d.folder === folder.name).length,
|
|
}))
|
|
);
|
|
}, [documents]);
|
|
|
|
const createDocument = useCallback(
|
|
async (doc: Omit<Document, 'id' | 'createdAt' | 'updatedAt' | 'size'>): Promise<Document | null> => {
|
|
// TODO: Implement via API
|
|
console.log('Create document not yet implemented');
|
|
return null;
|
|
},
|
|
[]
|
|
);
|
|
|
|
const updateDocument = useCallback(
|
|
async (id: string, updates: Partial<Omit<Document, 'id' | 'createdAt'>>): Promise<Document | null> => {
|
|
// TODO: Implement via API
|
|
console.log('Update document not yet implemented');
|
|
return null;
|
|
},
|
|
[]
|
|
);
|
|
|
|
const deleteDocument = useCallback(async (id: string): Promise<boolean> => {
|
|
try {
|
|
const response = await fetchWithTimeout(`/api/documents?id=${encodeURIComponent(id)}`, {
|
|
method: 'DELETE',
|
|
}, 10000);
|
|
|
|
let result: any = null;
|
|
try {
|
|
result = await response.json();
|
|
} catch {
|
|
// Best effort parse; fall back to status text below.
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(result?.error || `HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
setDocuments((prev) => prev.filter((doc) => doc.id !== id));
|
|
setError(null);
|
|
return true;
|
|
} catch (err: any) {
|
|
console.error('[useDocuments] Delete failed:', err);
|
|
setError(err?.message || 'Failed to delete document');
|
|
return false;
|
|
}
|
|
}, []);
|
|
|
|
const createFolder = useCallback((name: string): Folder => {
|
|
const newFolder: Folder = {
|
|
id: generateId(),
|
|
name,
|
|
count: 0,
|
|
};
|
|
setFolders((prev) => [...prev, newFolder]);
|
|
return newFolder;
|
|
}, []);
|
|
|
|
const deleteFolder = useCallback(
|
|
async (folderId: string): Promise<boolean> => {
|
|
const folder = folders.find((f) => f.id === folderId);
|
|
if (!folder) return false;
|
|
setFolders((prev) => prev.filter((f) => f.id !== folderId));
|
|
return true;
|
|
},
|
|
[folders]
|
|
);
|
|
|
|
const getDocumentById = useCallback(
|
|
(id: string): Document | undefined => {
|
|
return documents.find((d) => d.id === id);
|
|
},
|
|
[documents]
|
|
);
|
|
|
|
const getRecentDocuments = useCallback(
|
|
(limit = 5): Document[] => {
|
|
return [...documents]
|
|
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
|
.slice(0, limit);
|
|
},
|
|
[documents]
|
|
);
|
|
|
|
const getAllTags = useCallback((): string[] => {
|
|
const tags = new Set<string>();
|
|
documents.forEach((doc) => doc.tags.forEach((tag) => tags.add(tag)));
|
|
return Array.from(tags).sort();
|
|
}, [documents]);
|
|
|
|
// Refresh documents from API
|
|
const refreshDocuments = useCallback(async () => {
|
|
setIsLoaded(false);
|
|
// Re-trigger the effect by toggling a state (simplified)
|
|
window.location.reload();
|
|
}, []);
|
|
|
|
return {
|
|
documents,
|
|
folders,
|
|
isLoaded,
|
|
error,
|
|
createDocument,
|
|
updateDocument,
|
|
deleteDocument,
|
|
createFolder,
|
|
deleteFolder,
|
|
getDocumentById,
|
|
getRecentDocuments,
|
|
getAllTags,
|
|
refreshDocuments,
|
|
};
|
|
}
|