diff --git a/amplify.yml b/amplify.yml index e69489b..f5a287c 100644 --- a/amplify.yml +++ b/amplify.yml @@ -5,6 +5,7 @@ backend: commands: - npm ci --cache .npm --prefer-offline - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID + - npm i frontend: phases: build: diff --git a/amplify/backend.ts b/amplify/backend.ts index 56375d2..5150ae5 100644 --- a/amplify/backend.ts +++ b/amplify/backend.ts @@ -1,8 +1,16 @@ -import { defineBackend } from '@aws-amplify/backend'; -import { auth } from './auth/resource.js'; -import { data } from './data/resource.js'; +import { defineBackend } from "@aws-amplify/backend"; +import { auth } from "./auth/resource"; +import { data } from "./data/resource"; +import { deleteDocument } from "./functions/deleteDocument/resource"; +import { downloadDocument } from "./functions/downloadDocument/resource"; +import { fetchDocuments } from "./functions/fetchDocuments/resource"; +import { createDocument } from "./functions/createDocument/resource"; defineBackend({ auth, data, -}); + deleteDocument, + downloadDocument, + fetchDocuments, + createDocument +}); \ No newline at end of file diff --git a/amplify/data/resource.ts b/amplify/data/resource.ts index 5cead7a..72887d0 100644 --- a/amplify/data/resource.ts +++ b/amplify/data/resource.ts @@ -7,7 +7,7 @@ const schema = a.schema({ content: a.string(), }) .authorization((allow) => [allow.authenticated()]), // or add allow.apiKey() here if needed -}); + }); export const data = defineData({ schema, // ✅ This now has a value diff --git a/amplify/functions/createDocument/handler.ts b/amplify/functions/createDocument/handler.ts new file mode 100644 index 0000000..b897b7a --- /dev/null +++ b/amplify/functions/createDocument/handler.ts @@ -0,0 +1,97 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb"; +import { randomUUID } from 'crypto'; + +const client = new DynamoDBClient({ region: "us-west-1" }); +const docClient = DynamoDBDocumentClient.from(client); + +// Get the DynamoDB table name from environment variables +const tableName = process.env.DOCUMENT_TABLE_NAME || "Document-vnyciacn2nca3b6znjb4pulud4-NONE"; + +export const handler = async (event: APIGatewayProxyEvent): Promise => { + console.log("Event received:", JSON.stringify(event, null, 2)); + + try { + // Parse document data from request body + if (!event.body) { + return { + statusCode: 400, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "POST" + }, + body: JSON.stringify({ error: "Request body is required" }) + }; + } + + const requestBody = JSON.parse(event.body); + + // Validate required fields + if (!requestBody.title) { + return { + statusCode: 400, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "POST" + }, + body: JSON.stringify({ error: "Document title is required" }) + }; + } + + // Create document with required fields and timestamps + const now = new Date().toISOString(); + const documentId = requestBody.id || randomUUID(); + + const document = { + __typename: "Document", + id: documentId, + title: requestBody.title, + content: requestBody.content || "", + createdAt: now, + updatedAt: now + }; + + // Put the document in DynamoDB + const putParams = { + TableName: tableName, + Item: document + }; + + await docClient.send(new PutCommand(putParams)); + + return { + statusCode: 201, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "POST" + }, + body: JSON.stringify({ + message: "Document created successfully", + document + }) + }; + } catch (error: unknown) { + console.error("Error creating document:", error); + + return { + statusCode: 500, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "POST" + }, + body: JSON.stringify({ + error: "Failed to create document", + details: error instanceof Error ? error.message : 'Unknown error' + }) + }; + } +}; \ No newline at end of file diff --git a/amplify/functions/createDocument/resource.ts b/amplify/functions/createDocument/resource.ts new file mode 100644 index 0000000..bc2a1c6 --- /dev/null +++ b/amplify/functions/createDocument/resource.ts @@ -0,0 +1,6 @@ +import { defineFunction } from "@aws-amplify/backend"; + +export const createDocument = defineFunction({ + name: "createDocument", + entry: "./handler.ts" +}); \ No newline at end of file diff --git a/amplify/functions/deleteDocument/handler.ts b/amplify/functions/deleteDocument/handler.ts new file mode 100644 index 0000000..9558e42 --- /dev/null +++ b/amplify/functions/deleteDocument/handler.ts @@ -0,0 +1,87 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { DynamoDBDocumentClient, DeleteCommand } from "@aws-sdk/lib-dynamodb"; + +const client = new DynamoDBClient({}); +const docClient = DynamoDBDocumentClient.from(client); + +// Get the DynamoDB table name from environment variables +const tableName = process.env.DOCUMENT_TABLE_NAME || "Document-vnyciacn2nca3b6znjb4pulud4-NONE"; + +export const handler = async (event: APIGatewayProxyEvent): Promise => { + console.log("Event received:", JSON.stringify(event, null, 2)); + + try { + // Extract document ID from event + const documentId = event.pathParameters?.id || + JSON.parse(event.body || '{}').id || + event.queryStringParameters?.id; + + if (!documentId) { + return { + statusCode: 400, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "DELETE" + }, + body: JSON.stringify({ error: "Document ID is required" }) + }; + } + + // Delete the document from DynamoDB + const deleteParams = { + TableName: tableName, + Key: { + id: documentId + }, + ReturnValues: "ALL_OLD" as const // Return the deleted item + }; + + const { Attributes } = await docClient.send(new DeleteCommand(deleteParams)); + + if (!Attributes) { + return { + statusCode: 404, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "DELETE" + }, + body: JSON.stringify({ error: "Document not found or already deleted" }) + }; + } + + return { + statusCode: 200, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "DELETE" + }, + body: JSON.stringify({ + message: "Document deleted successfully", + deletedDocument: Attributes + }) + }; + } catch (error: unknown) { + console.error("Error deleting document:", error); + + return { + statusCode: 500, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "DELETE" + }, + body: JSON.stringify({ + error: "Failed to delete document", + details: error instanceof Error ? error.message : 'Unknown error' + }) + }; + } +}; \ No newline at end of file diff --git a/amplify/functions/deleteDocument/package.json b/amplify/functions/deleteDocument/package.json deleted file mode 100644 index e69de29..0000000 diff --git a/amplify/functions/deleteDocument/resource.ts b/amplify/functions/deleteDocument/resource.ts new file mode 100644 index 0000000..47880ce --- /dev/null +++ b/amplify/functions/deleteDocument/resource.ts @@ -0,0 +1,6 @@ +import { defineFunction } from "@aws-amplify/backend"; + +export const deleteDocument = defineFunction({ + name: "deleteDocument", + entry: "./handler.ts" +}); \ No newline at end of file diff --git a/amplify/functions/downloadDocument/handler.ts b/amplify/functions/downloadDocument/handler.ts new file mode 100644 index 0000000..72cbc53 --- /dev/null +++ b/amplify/functions/downloadDocument/handler.ts @@ -0,0 +1,89 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb"; + +const client = new DynamoDBClient({}); +const docClient = DynamoDBDocumentClient.from(client); + +// Get the DynamoDB table name from environment variables +const tableName = process.env.DOCUMENT_TABLE_NAME || "Document-vnyciacn2nca3b6znjb4pulud4-NONE"; + +export const handler = async (event: APIGatewayProxyEvent): Promise => { + console.log("Event received:", JSON.stringify(event, null, 2)); + + try { + // Extract document ID from the event + const documentId = event.pathParameters?.id || + event.queryStringParameters?.id || + JSON.parse(event.body || '{}').id; + + if (!documentId) { + return { + statusCode: 400, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "GET" + }, + body: JSON.stringify({ error: "Document ID is required" }) + }; + } + + // Get the document from DynamoDB + const getParams = { + TableName: tableName, + Key: { + id: documentId + } + }; + + const { Item } = await docClient.send(new GetCommand(getParams)); + + if (!Item) { + return { + statusCode: 404, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "GET" + }, + body: JSON.stringify({ error: "Document not found" }) + }; + } + + // Return the document content + return { + statusCode: 200, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "GET", + "Content-Disposition": `attachment; filename="${Item.title || 'document'}.txt"` + }, + body: JSON.stringify({ + document: Item, + content: Item.content || "", + title: Item.title || "document" + }) + }; + } catch (error: unknown) { + console.error("Error downloading document:", error); + + return { + statusCode: 500, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "GET" + }, + body: JSON.stringify({ + error: "Failed to download document", + details: error instanceof Error ? error.message : 'Unknown error' + }) + }; + } +}; \ No newline at end of file diff --git a/amplify/functions/downloadDocument/package.json b/amplify/functions/downloadDocument/package.json deleted file mode 100644 index e69de29..0000000 diff --git a/amplify/functions/downloadDocument/resource.ts b/amplify/functions/downloadDocument/resource.ts new file mode 100644 index 0000000..33a9dc0 --- /dev/null +++ b/amplify/functions/downloadDocument/resource.ts @@ -0,0 +1,6 @@ +import { defineFunction } from "@aws-amplify/backend"; + +export const downloadDocument = defineFunction({ + name: "downloadDocument", + entry: "./handler.ts" +}); \ No newline at end of file diff --git a/amplify/functions/fetchDocuments/handler.ts b/amplify/functions/fetchDocuments/handler.ts new file mode 100644 index 0000000..b7cdeb5 --- /dev/null +++ b/amplify/functions/fetchDocuments/handler.ts @@ -0,0 +1,52 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb"; + +const client = new DynamoDBClient({}); +const docClient = DynamoDBDocumentClient.from(client); + +// Get the DynamoDB table name from environment variables +const tableName = process.env.DOCUMENT_TABLE_NAME || "Document-vnyciacn2nca3b6znjb4pulud4-NONE"; + +export const handler = async (event: APIGatewayProxyEvent): Promise => { + console.log("Event received:", JSON.stringify(event, null, 2)); + + try { + // Scan the DynamoDB table to get all documents + const scanParams = { + TableName: tableName, + }; + + const scanResponse = await docClient.send(new ScanCommand(scanParams)); + + // Return the results + return { + statusCode: 200, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", // For CORS support + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "GET" + }, + body: JSON.stringify({ + documents: scanResponse.Items || [], + count: scanResponse.Count || 0 + }) + }; + } catch (error: unknown) { + console.error("Error fetching documents:", error); + return { + statusCode: 500, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Methods": "GET" + }, + body: JSON.stringify({ + error: "Failed to fetch documents", + details: error instanceof Error ? error.message : 'Unknown error' + }) + }; + } +}; \ No newline at end of file diff --git a/amplify/functions/fetchDocuments/index.mjs b/amplify/functions/fetchDocuments/index.mjs index fe59eeb..f2e65f5 100644 --- a/amplify/functions/fetchDocuments/index.mjs +++ b/amplify/functions/fetchDocuments/index.mjs @@ -7,7 +7,7 @@ const client = new DynamoDBClient({}); const docClient = DynamoDBDocumentClient.from(client); // Get the DynamoDB table name from environment variables -const tableName = process.env.DOCUMENT_TABLE_NAME; +const tableName = "Document-nu434abnqjhf3kcbgxbcibzamu-NONE"; export const handler = async (event) => { console.log("Event received:", JSON.stringify(event, null, 2)); diff --git a/amplify/functions/fetchDocuments/package.json b/amplify/functions/fetchDocuments/package.json deleted file mode 100644 index e69de29..0000000 diff --git a/amplify/functions/fetchDocuments/resource.ts b/amplify/functions/fetchDocuments/resource.ts new file mode 100644 index 0000000..3bcd80f --- /dev/null +++ b/amplify/functions/fetchDocuments/resource.ts @@ -0,0 +1,6 @@ +import { defineFunction } from "@aws-amplify/backend"; + +export const fetchDocuments = defineFunction({ + name: "fetchDocuments", + entry: "./handler.ts" +}); \ No newline at end of file diff --git a/app/api/documents/[id]/route.ts b/app/api/documents/[id]/route.ts new file mode 100644 index 0000000..5ade3e0 --- /dev/null +++ b/app/api/documents/[id]/route.ts @@ -0,0 +1,66 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + // Get basic information for debugging + const id = params.id; + const apiEndpoint = process.env.NEXT_PUBLIC_LAMBDA_ENDPOINT; + const url = apiEndpoint ? `${apiEndpoint}/documents/${id}` : "No API endpoint configured"; + + // Create environment variables object with proper typing + const envVars: Record = {}; + Object.keys(process.env) + .filter(key => key.startsWith('NEXT_') || key.startsWith('LAMBDA_')) + .forEach(key => { + envVars[key] = process.env[key]; + }); + + // Return a simple success response with debug information + return NextResponse.json({ + message: "API route is functioning correctly", + debug_info: { + id: id, + timestamp: new Date().toISOString(), + api_endpoint_configured: !!apiEndpoint, + api_endpoint_value: apiEndpoint || "NOT SET", + request_url: url, + env_vars: envVars + } + }); +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + // Similar simplified response for DELETE + return NextResponse.json({ + message: "DELETE route is functioning correctly", + id: params.id, + api_endpoint: process.env.NEXT_PUBLIC_LAMBDA_ENDPOINT || "NOT SET" + }); +} + +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + // Get request body for PUT + let body = null; + try { + body = await request.json(); + } catch (error) { + // If body parsing fails, just note it + body = "Failed to parse request body"; + } + + // Return simple success response with debug info + return NextResponse.json({ + message: "PUT route is functioning correctly", + id: params.id, + api_endpoint: process.env.NEXT_PUBLIC_LAMBDA_ENDPOINT || "NOT SET", + received_body: body + }); +} \ No newline at end of file diff --git a/app/api/documents/route.ts b/app/api/documents/route.ts new file mode 100644 index 0000000..a4d7563 --- /dev/null +++ b/app/api/documents/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET() { + const apiEndpoint = process.env.NEXT_PUBLIC_LAMBDA_ENDPOINT; + + // Create environment variables object with proper typing + const envVars: Record = {}; + Object.keys(process.env) + .filter(key => key.startsWith('NEXT_') || key.startsWith('LAMBDA_')) + .forEach(key => { + envVars[key] = process.env[key]; + }); + + return NextResponse.json({ + message: "Documents list API route is functioning correctly", + debug_info: { + timestamp: new Date().toISOString(), + api_endpoint_configured: !!apiEndpoint, + api_endpoint_value: apiEndpoint || "NOT SET", + env_vars: envVars + } + }); +} + +export async function POST(request: NextRequest) { + let body = null; + try { + body = await request.json(); + } catch (error) { + body = "Failed to parse request body"; + } + + return NextResponse.json({ + message: "POST route is functioning correctly", + api_endpoint: process.env.NEXT_PUBLIC_LAMBDA_ENDPOINT || "NOT SET", + received_body: body + }); +} \ No newline at end of file diff --git a/app/document-selector/DocumentSelector.tsx b/app/document-selector/DocumentSelector.tsx index d0296ba..ae86a23 100644 --- a/app/document-selector/DocumentSelector.tsx +++ b/app/document-selector/DocumentSelector.tsx @@ -2,6 +2,10 @@ import { useState, useEffect } from "react"; import DocumentTile from "./DocumentTile"; +import Editor from "../editor/page"; +import { generateClient } from "aws-amplify/data"; +import type {Schema} from "../../amplify/data/resource"; +import { ModelField, Nullable } from "@aws-amplify/data-schema"; interface Document { id: string; @@ -14,37 +18,46 @@ interface DocumentSelectorProps { signOut?: () => void; } +const client = generateClient(); + export default function DocumentSelector({ signOut }: DocumentSelectorProps) { const [documents, setDocuments] = useState([]); const [loading, setLoading] = useState(false); - + const fetchDocuments = async () => { setLoading(true); try { - const response = await fetch("/api/fetchDocuments"); + const response = await fetch('/api/documents'); const data = await response.json(); - setDocuments(data); + if (data && data.documents) { + setDocuments(data.documents); + } + console.log("Fetched documents:", data); } catch (error) { console.error("Failed to fetch documents:", error); } finally { setLoading(false); } }; - + const createDocument = async () => { - const title = prompt("Enter a title for the new document:"); - if (!title) return; - - try { - const response = await fetch("/api/createDocument", { - method: "POST", - body: JSON.stringify({ title }), - }); - - const newDoc = await response.json(); - window.location.href = `/editor?docId=${newDoc.id}`; - } catch (error) { - console.error("Failed to create document:", error); + const title = window.prompt("Create New Document"); + if (title) { + try { + const response = await fetch('/api/documents', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ title }), + }); + + if (response.ok) { + fetchDocuments(); // Refresh the list + } + } catch (error) { + console.error("Failed to create document:", error); + } } }; @@ -82,6 +95,7 @@ export default function DocumentSelector({ signOut }: DocumentSelectorProps) { /> )) )} + diff --git a/app/document-selector/DocumentTile.tsx b/app/document-selector/DocumentTile.tsx index d8991c5..da4d710 100644 --- a/app/document-selector/DocumentTile.tsx +++ b/app/document-selector/DocumentTile.tsx @@ -1,5 +1,8 @@ import Link from "next/link"; import styles from "./DocumentTile.module.css"; +import { generateClient } from "aws-amplify/data"; +import type {Schema} from "../../amplify/data/resource"; +const client = generateClient(); interface DocumentTileProps { id: string; @@ -12,10 +15,30 @@ export default function DocumentTile({ title, createdAt, }: DocumentTileProps) { + const delete_doc = async () => { + try { + console.log("Deleting document:", id); + const response = await fetch(`/api/documents/${id}`, { + method: 'DELETE', + }); + + if (response.ok) { + // Refresh the document list or navigate away + window.location.reload(); + } else { + console.error("Failed to delete document"); + } + } catch (error) { + console.error("Error deleting document:", error); + } + }; return ( - +
+

{title}

{new Date(createdAt).toLocaleString()}

+ +
); } diff --git a/app/document-selector/page.tsx b/app/document-selector/page.tsx index 8460283..837da02 100644 --- a/app/document-selector/page.tsx +++ b/app/document-selector/page.tsx @@ -1,13 +1,245 @@ "use client"; +import "@/lib/amplifyClient"; +import React, { + useState, + useEffect, + useRef, + FC, + ChangeEvent, + Dispatch, + SetStateAction, +} from "react"; import { Authenticator } from "@aws-amplify/ui-react"; import "@aws-amplify/ui-react/styles.css"; -import DocumentSelector from "./DocumentSelector"; -export default function Page() { +const Editor: FC = () => { + // Get document ID from URL params + let url_params: URLSearchParams; + let docId; + if (typeof window !== "undefined") { + url_params = new URLSearchParams(window.location.search); + docId = url_params.get("docId"); + } else { + docId = null; + } + if (docId === null) { + docId = "848cca7a-3bf8-443f-aa9a-2f18a185189f"; + } + console.log("Document ID:", docId); + + const [content, setContent] = useState(""); + const [title, setTitle] = useState(""); + const [bold, setBold] = useState(false); + const [italic, setItalic] = useState(false); + const [underline, setUnderline] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const saveTimeout = useRef(null); + + // Function to fetch document using downloadDocument Lambda + const fetchDocument = async () => { + try { + setLoading(true); + const response = await fetch(`/api/documents/${docId}`); + + if (!response.ok) { + throw new Error(`Error: ${response.status}`); + } + + const data = await response.json(); + + if (data && data.document) { + setContent(data.document.content || ""); + setTitle(data.document.title || "Untitled Document"); + } else { + setError("Document not found"); + } + } catch (error) { + console.error("Error fetching document:", error); + setError("Failed to load document"); + } finally { + setLoading(false); + } + }; + + // Function to handle content edits + const handleEdit = async (event: ChangeEvent) => { + const updatedContent = event.target.value; + setContent(updatedContent); + + // Debounce updates to avoid excessive API calls + if (saveTimeout.current) { + clearTimeout(saveTimeout.current); + } + + saveTimeout.current = setTimeout(async () => { + try { + const response = await fetch(`/api/documents/${docId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + content: updatedContent, + id: docId + }), + }); + + if (!response.ok) { + console.error("Failed to update document"); + } + } catch (error) { + console.error("Error updating document:", error); + } + }, 1000); + }; + + // Function to download the document + const handleDownload = async () => { + try { + // Use downloadDocument Lambda function + const response = await fetch(`/api/documents/${docId}`); + + if (!response.ok) { + throw new Error(`Error: ${response.status}`); + } + + const data = await response.json(); + + if (!data || !data.document) { + alert("No document found."); + return; + } + + // Create downloadable file + const blob = new Blob([data.document.content || ""], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.href = url; + link.download = `${data.document.title || "document"}.txt`; + document.body.appendChild(link); + link.click(); + + document.body.removeChild(link); + URL.revokeObjectURL(url); + } catch (error) { + console.error("Download failed:", error); + alert("Failed to download document."); + } + }; + + // Toggle text formatting styles + const toggleStyle = ( + style: boolean, + setter: Dispatch>, + eventName: string + ): void => { + setter((prev: boolean) => !prev); + }; + + // Initial document fetch + useEffect(() => { + fetchDocument(); + + // Set up polling for real-time updates (every 5 seconds) + const interval = setInterval(() => { + fetchDocument(); + }, 5000); + + // Clean up on unmount + return () => { + clearInterval(interval); + if (saveTimeout.current) { + clearTimeout(saveTimeout.current); + } + }; + }, [docId]); + + if (loading && !content) { + return ( +
+
+

Loading document...

+
+
+ ); + } + + if (error) { + return ( +
+
+

Error

+

{error}

+ +
+
+ ); + } + return ( - {({ signOut }) => } + {({ signOut }) => ( +
+
+

{title || "Real-time Collaborative Editor"}

+ + {/* Formatting Controls */} +
+ + + +
+ + {/* Editor Text Area */} +
+