| import { useState, useEffect, useRef } from "react"; |
| import { Document, AuthUser, SyncLog, TeamMember } from "../types"; |
| import { INITIAL_DOCUMENTS, INITIAL_TEAM_MEMBERS } from "../data"; |
| import { DEFAULT_VALUES } from "../config"; |
|
|
| export function useWorkspaceState() { |
| |
| const [documents, setDocuments] = useState<Document[]>(() => { |
| const cached = localStorage.getItem("workspace_documents"); |
| return cached ? JSON.parse(cached) : INITIAL_DOCUMENTS; |
| }); |
| const [activeDocId, setActiveDocId] = useState<string>(""); |
|
|
| const activeDoc = |
| documents.find((d) => d.id === activeDocId) || documents[0] || INITIAL_DOCUMENTS[0]; |
|
|
| const [editorText, setEditorText] = useState(activeDoc.content); |
|
|
| |
| const [isOnline, setIsOnline] = useState(navigator.onLine); |
| const [syncStatus, setSyncStatus] = useState<"synced" | "syncing" | "offline" | "error">("synced"); |
| const [syncLogs, setSyncLogs] = useState<SyncLog[]>([]); |
|
|
| |
| const [user, setUser] = useState<AuthUser>(() => { |
| const cached = localStorage.getItem("workspace_user"); |
| return cached |
| ? JSON.parse(cached) |
| : { |
| email: null, |
| displayName: null, |
| isLoggedIn: false, |
| tier: DEFAULT_VALUES.TIER, |
| privateKeySeed: "", |
| }; |
| }); |
|
|
| |
| const [teamMembers, setTeamMembers] = useState<TeamMember[]>(INITIAL_TEAM_MEMBERS); |
| const [teamChangesEnabled, setTeamChangesEnabled] = useState(true); |
| const [activeCollaborators, setActiveCollaborators] = useState(1); |
|
|
| |
| const [toast, setToast] = useState<{ |
| show: boolean; |
| message: string; |
| timestamp: string; |
| }>({ show: false, message: "", timestamp: "" }); |
|
|
| |
| const [showDocModal, setShowDocModal] = useState(false); |
| const [newDocTitle, setNewDocTitle] = useState(""); |
| const [documentSearchTerm, setDocumentSearchTerm] = useState(""); |
| const [uploadFile, setUploadFile] = useState<File | null>(null); |
| const [uploadLoading, setUploadLoading] = useState(false); |
| const [uploadError, setUploadError] = useState<string | null>(null); |
|
|
| |
| const [adminPassword, setAdminPassword] = useState(""); |
| const [isAdminLoggedIn, setIsAdminLoggedIn] = useState(false); |
| const [adminVerifyLoading] = useState(false); |
|
|
| |
| const [tokensConsumed, setTokensConsumed] = useState(0); |
|
|
| |
| const [showVersionHistory, setShowVersionHistory] = useState(false); |
| const [isReadingMode, setIsReadingMode] = useState(false); |
| const [showExportPreview, setShowExportPreview] = useState(false); |
| const [exportPreviewFormat, setExportPreviewFormat] = useState<"markdown" | "pdf" | "html">("pdf"); |
| const [showPreferenceModal, setShowPreferenceModal] = useState(false); |
| const [showShareModal, setShowShareModal] = useState(false); |
| const [isSidebarOpen, setIsSidebarOpen] = useState(true); |
| const [copiedDoc, setCopiedDoc] = useState(false); |
| const [showStatsPopover, setShowStatsPopover] = useState(false); |
| const [leftPanelMode, setLeftPanelMode] = useState<"edit" | "preview" | "folding">("edit"); |
| const [foldedBlockIds, setFoldedBlockIds] = useState<Record<string, boolean>>({}); |
|
|
| |
| const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null); |
| const isFirstLocalStorageSave = useRef(true); |
|
|
| |
| useEffect(() => { |
| setEditorText(activeDoc.content); |
| }, [activeDoc.id]); |
|
|
| |
| useEffect(() => { |
| const urlStr = window.location.href; |
| const match = urlStr.match(/\/share\/doc\/([^/?#]+)/); |
| if (match) { |
| const docId = match[1]; |
| |
| import("../lib/apiClient").then(({ getDocument }) => { |
| getDocument(docId) |
| .then((serverDoc) => { |
| const docExists = documents.some((d) => d.id === docId); |
| if (!docExists) { |
| const loadedDoc: Document = { |
| id: serverDoc.id, |
| title: serverDoc.title, |
| content: serverDoc.content, |
| version: serverDoc.version, |
| lastSaved: serverDoc.lastSaved, |
| isSynced: true |
| }; |
| setDocuments((prev) => [loadedDoc, ...prev]); |
| } |
| setActiveDocId(docId); |
| setIsReadingMode(true); |
| addLog(`Shared document loaded successfully.`, "success"); |
| }) |
| .catch(() => { |
| addLog(`Shared document not found on server.`, "error"); |
| }); |
| }); |
| } |
| }, []); |
|
|
| |
| useEffect(() => { |
| async function initAuth() { |
| |
| const urlParams = new URLSearchParams(window.location.search); |
| const oauthCode = urlParams.get("code"); |
| if (oauthCode) { |
| |
| const provider = sessionStorage.getItem("oauth_provider") || "google"; |
| const redirectUri = sessionStorage.getItem("oauth_redirect_uri") || `${window.location.origin}/auth/callback`; |
| sessionStorage.removeItem("oauth_provider"); |
| sessionStorage.removeItem("oauth_redirect_uri"); |
| try { |
| const { oauthCallback } = await import("../lib/apiClient"); |
| const res = await oauthCallback(provider, oauthCode, redirectUri); |
| localStorage.setItem("workspace_auth_token", res.token); |
| setUser({ |
| email: res.user.email, |
| displayName: res.user.display_name, |
| isLoggedIn: true, |
| isGuest: false, |
| tier: res.user.tier, |
| privateKeySeed: "", |
| hasPasskey: res.user.has_passkey, |
| oauthProvider: res.user.oauth_provider, |
| }); |
| addLog(`Signed in via ${provider}: ${res.user.display_name}`, "success"); |
| |
| window.history.replaceState({}, "", window.location.pathname); |
| } catch (err: any) { |
| addLog(`OAuth callback failed: ${err.message}`, "error"); |
| } |
| return; |
| } |
|
|
| |
| const existingToken = localStorage.getItem("workspace_auth_token"); |
| if (existingToken) { |
| const cached = localStorage.getItem("workspace_user"); |
| const parsed = cached ? JSON.parse(cached) : null; |
| if (parsed && parsed.isLoggedIn) { |
| addLog(`Restored session for ${parsed.displayName || parsed.email}`, "success"); |
| return; |
| } |
| } |
|
|
| |
| let deviceUuid = localStorage.getItem("workspace_device_uuid"); |
| if (!deviceUuid) { |
| deviceUuid = (typeof crypto.randomUUID === "function") |
| ? crypto.randomUUID() |
| : `device-${Math.random().toString(36).substring(2, 15)}`; |
| localStorage.setItem("workspace_device_uuid", deviceUuid); |
| } |
|
|
| try { |
| const { authGuest } = await import("../lib/apiClient"); |
| const res = await authGuest(deviceUuid); |
| localStorage.setItem("workspace_auth_token", res.token); |
| setUser({ |
| email: res.user.email, |
| displayName: res.user.display_name, |
| isLoggedIn: true, |
| isGuest: res.user.is_guest, |
| tier: res.user.tier, |
| privateKeySeed: "", |
| }); |
| addLog(`Guest session: ${res.user.display_name}`, "success"); |
|
|
| |
| try { |
| const { getDocuments } = await import("../lib/apiClient"); |
| const docRes = await getDocuments(); |
| if (docRes.documents && docRes.documents.length > 0) { |
| const serverDocs: Document[] = docRes.documents.map((d: any) => ({ |
| id: d.id, |
| title: d.title, |
| content: d.content, |
| version: d.version, |
| lastSaved: d.lastSaved, |
| isSynced: true |
| })); |
| setDocuments(serverDocs); |
| } |
| } catch { |
| |
| } |
| } catch (err: any) { |
| addLog(`Auth failed: ${err.message}`, "error"); |
| } |
| } |
|
|
| initAuth(); |
| }, []); |
|
|
| |
| useEffect(() => { |
| const handleOnline = () => { |
| setIsOnline(true); |
| setSyncStatus("synced"); |
| addLog("Network connection restored. Re-authenticating sync tunnel.", "success"); |
| }; |
| const handleOffline = () => { |
| setIsOnline(false); |
| setSyncStatus("offline"); |
| addLog("Offline mode active. All edits are being buffered securely in local IndexDB.", "info"); |
| }; |
| window.addEventListener("online", handleOnline); |
| window.addEventListener("offline", handleOffline); |
| return () => { |
| window.removeEventListener("online", handleOnline); |
| window.removeEventListener("offline", handleOffline); |
| }; |
| }, []); |
|
|
| |
| const [autoSaveEnabled, setAutoSaveEnabled] = useState<boolean>(() => { |
| return localStorage.getItem("workspace_pref_autoSave") !== "false"; |
| }); |
|
|
| useEffect(() => { |
| if (!autoSaveEnabled) return; |
| if (isFirstLocalStorageSave.current) { |
| isFirstLocalStorageSave.current = false; |
| return; |
| } |
| const timer = setTimeout(() => { |
| localStorage.setItem("workspace_documents", JSON.stringify(documents)); |
| const now = new Date(); |
| const formattedTime = now.toLocaleTimeString([], { |
| hour: "2-digit", |
| minute: "2-digit", |
| second: "2-digit", |
| }); |
| setToast({ |
| show: true, |
| message: "All document progress autosaved locally", |
| timestamp: formattedTime, |
| }); |
| }, 1500); |
| return () => clearTimeout(timer); |
| }, [documents, autoSaveEnabled]); |
|
|
| |
| useEffect(() => { |
| if (toast.show) { |
| const timer = setTimeout(() => { |
| setToast((prev) => ({ ...prev, show: false })); |
| }, 3000); |
| return () => clearTimeout(timer); |
| } |
| }, [toast.show]); |
|
|
| |
| useEffect(() => { |
| const handleBeforeUnload = () => { |
| if (autoSaveEnabled) { |
| localStorage.setItem("workspace_documents", JSON.stringify(documents)); |
| } |
| }; |
| window.addEventListener("beforeunload", handleBeforeUnload); |
| return () => window.removeEventListener("beforeunload", handleBeforeUnload); |
| }, [documents, autoSaveEnabled]); |
|
|
| |
| useEffect(() => { |
| localStorage.setItem("workspace_user", JSON.stringify(user)); |
| }, [user]); |
|
|
| |
| const addLog = (text: string, type: "success" | "info" | "sync" | "error") => { |
| setSyncLogs((prev) => [ |
| { |
| id: `log-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, |
| text, |
| timestamp: new Date().toISOString(), |
| type, |
| }, |
| ...prev.slice(0, 24), |
| ]); |
| }; |
|
|
| const handleManualSave = () => { |
| localStorage.setItem("workspace_documents", JSON.stringify(documents)); |
| const now = new Date(); |
| const formattedTime = now.toLocaleTimeString([], { |
| hour: "2-digit", |
| minute: "2-digit", |
| second: "2-digit", |
| }); |
| setToast({ |
| show: true, |
| message: "Document progress manually saved locally", |
| timestamp: formattedTime, |
| }); |
| addLog("Document progress manually saved locally.", "success"); |
| }; |
|
|
| return { |
| documents, |
| setDocuments, |
| activeDocId, |
| setActiveDocId, |
| activeDoc, |
| editorText, |
| setEditorText, |
| isOnline, |
| syncStatus, |
| setSyncStatus, |
| syncLogs, |
| user, |
| setUser, |
| teamMembers, |
| setTeamMembers, |
| teamChangesEnabled, |
| setTeamChangesEnabled, |
| activeCollaborators, |
| setActiveCollaborators, |
| toast, |
| setToast, |
| showDocModal, |
| setShowDocModal, |
| newDocTitle, |
| setNewDocTitle, |
| documentSearchTerm, |
| setDocumentSearchTerm, |
| uploadFile, |
| setUploadFile, |
| uploadLoading, |
| setUploadLoading, |
| uploadError, |
| setUploadError, |
| adminPassword, |
| setAdminPassword, |
| isAdminLoggedIn, |
| setIsAdminLoggedIn, |
| adminVerifyLoading, |
| tokensConsumed, |
| setTokensConsumed, |
| showVersionHistory, |
| setShowVersionHistory, |
| isReadingMode, |
| setIsReadingMode, |
| showExportPreview, |
| setShowExportPreview, |
| exportPreviewFormat, |
| setExportPreviewFormat, |
| showPreferenceModal, |
| setShowPreferenceModal, |
| showShareModal, |
| setShowShareModal, |
| isSidebarOpen, |
| setIsSidebarOpen, |
| copiedDoc, |
| setCopiedDoc, |
| showStatsPopover, |
| setShowStatsPopover, |
| leftPanelMode, |
| setLeftPanelMode, |
| foldedBlockIds, |
| setFoldedBlockIds, |
| autoSaveEnabled, |
| setAutoSaveEnabled, |
| saveTimeoutRef, |
| isFirstLocalStorageSave, |
| addLog, |
| handleManualSave, |
| }; |
| } |