diff --git a/backend/create_blanks.go b/backend/create_blanks.go
new file mode 100644
index 0000000..ea0c318
--- /dev/null
+++ b/backend/create_blanks.go
@@ -0,0 +1,7 @@
+package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("Running...")
+}
diff --git a/backend/handlers/onlyoffice.go b/backend/handlers/onlyoffice.go
index f6defff..52a98fd 100644
--- a/backend/handlers/onlyoffice.go
+++ b/backend/handlers/onlyoffice.go
@@ -166,6 +166,67 @@ func (h *OnlyOfficeHandler) CreateBlank(c *fiber.Ctx) error {
})
}
+// POST /api/onlyoffice/template
+func (h *OnlyOfficeHandler) CreateFromTemplate(c *fiber.Ctx) error {
+ var req struct {
+ TemplateName string `json:"template"` // e.g., "apa.docx"
+ }
+ if err := c.BodyParser(&req); err != nil {
+ return c.Status(400).JSON(fiber.Map{"error": "invalid request"})
+ }
+
+ srcPath := filepath.Join("templates", req.TemplateName)
+ if _, err := os.Stat(srcPath); os.IsNotExist(err) {
+ return c.Status(404).JSON(fiber.Map{"error": "template not found"})
+ }
+
+ // Figure out a safe name in root directory
+ baseName := "Untitled Document"
+ ext := ".docx"
+ if strings.HasSuffix(req.TemplateName, ".pptx") {
+ baseName = "Untitled Presentation"
+ ext = ".pptx"
+ }
+ if req.TemplateName == "apa.docx" {
+ baseName = "APA Paper"
+ } else if req.TemplateName == "resume.docx" {
+ baseName = "Resume"
+ }
+
+ finalPath := filepath.Join(h.Config.StorageDir, baseName+ext)
+ counter := 1
+ for {
+ if _, err := os.Stat(finalPath); os.IsNotExist(err) {
+ break
+ }
+ finalPath = filepath.Join(h.Config.StorageDir, fmt.Sprintf("%s (%d)%s", baseName, counter, ext))
+ counter++
+ }
+
+ // Copy the template file
+ srcFile, err := os.Open(srcPath)
+ if err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": "failed to open template"})
+ }
+ defer srcFile.Close()
+
+ destFile, err := os.Create(finalPath)
+ if err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": "failed to create destination file"})
+ }
+ defer destFile.Close()
+
+ if _, err := io.Copy(destFile, srcFile); err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": "failed to copy template data"})
+ }
+
+ relativePath, _ := filepath.Rel(h.Config.StorageDir, finalPath)
+ return c.JSON(fiber.Map{
+ "path": filepath.ToSlash(relativePath),
+ "name": filepath.Base(finalPath),
+ })
+}
+
// ListOfficeFiles returns all .docx or .pptx files across the entire storage directory.
// GET /api/onlyoffice/files?type=docx|pptx
func (h *OnlyOfficeHandler) ListOfficeFiles(c *fiber.Ctx) error {
diff --git a/backend/main.go b/backend/main.go
index 4948066..55452ce 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -193,6 +193,7 @@ func main() {
// OnlyOffice
protected.Post("/onlyoffice/create", onlyOfficeHandler.CreateBlank)
+ protected.Post("/onlyoffice/template", onlyOfficeHandler.CreateFromTemplate)
protected.Post("/onlyoffice/sign", onlyOfficeHandler.SignConfig)
protected.Get("/onlyoffice/files", onlyOfficeHandler.ListOfficeFiles)
diff --git a/backend/templates/apa.docx b/backend/templates/apa.docx
new file mode 100644
index 0000000..5bb59be
--- /dev/null
+++ b/backend/templates/apa.docx
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/backend/templates/resume.docx b/backend/templates/resume.docx
new file mode 100644
index 0000000..5bb59be
--- /dev/null
+++ b/backend/templates/resume.docx
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/frontend/src/components/FileExplorer.tsx b/frontend/src/components/FileExplorer.tsx
index 70fdc26..d3464be 100644
--- a/frontend/src/components/FileExplorer.tsx
+++ b/frontend/src/components/FileExplorer.tsx
@@ -1578,7 +1578,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
)}
- {pathSegments.map((seg, i) => (
-
- {i > 0 && }
-
-
- ))}
+ {appMode === 'drive' ? (
+ pathSegments.map((seg, i) => (
+
+ {i > 0 && }
+
+
+ ))
+ ) : (
+
+ {appMode === 'docs' ? 'Documents' : 'Presentations'}
+
+ )}
)}
- {activeSection !== 'settings' && (
+ {activeSection !== 'settings' && appMode === 'drive' && (
{!isMobile || mobileSearchOpen ? (
<>
@@ -1799,31 +1805,35 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
-
-
-
-
- {['name', 'size', 'date', 'type'].map(opt => (
-
- ))}
+ {appMode === 'drive' && (
+ <>
+
+
+
+
+ {['name', 'size', 'date', 'type'].map(opt => (
+
+ ))}
+
+
-
-
-
+
+ >
+ )}
{activeSection === 'files' && (
+ {/* Pinned Documents */}
+ {pinnedFiles.length > 0 && (
+
+
+
Pinned {title.toLowerCase()}
+
+ {pinnedFiles.map(file => renderFileCard(file))}
+
+
+
+ )}
+
{/* Recent Documents */}
@@ -285,8 +376,21 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank }: Office
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-black/5 transition-colors"
style={{ color: 'var(--color-text-primary)' }}
onClick={() => {
+ handlePin(contextMenu.file);
+ setContextMenu(null);
+ }}
+ >
+
+ {contextMenu.file.is_pinned || pinnedFiles.find(f => f.path === contextMenu.file.path) ? 'Unpin' : 'Pin'}
+
+
{
+ const extMatch = contextMenu.file.name.match(/\.[^.]+$/);
+ const baseName = extMatch ? contextMenu.file.name.slice(0, -extMatch[0].length) : contextMenu.file.name;
setRenaming(contextMenu.file.path);
- setRenameValue(contextMenu.file.name);
+ setRenameValue(baseName);
setContextMenu(null);
}}
>
diff --git a/frontend/src/components/OnlyOfficeEditor.tsx b/frontend/src/components/OnlyOfficeEditor.tsx
index 1bae143..ac6c10e 100644
--- a/frontend/src/components/OnlyOfficeEditor.tsx
+++ b/frontend/src/components/OnlyOfficeEditor.tsx
@@ -25,16 +25,33 @@ export default function OnlyOfficeEditor({ file, type, onClose, onRename }: Only
const [loadError, setLoadError] = useState(null);
const [config, setConfig] = useState(null);
const [isRenaming, setIsRenaming] = useState(false);
- const [renameValue, setRenameValue] = useState(file.name);
+
+ const extMatch = file.name.match(/\.[^.]+$/);
+ const ext = extMatch ? extMatch[0] : '';
+ const baseName = extMatch ? file.name.slice(0, -ext.length) : file.name;
+
+ const [renameValue, setRenameValue] = useState(baseName);
const renameInputRef = useRef(null);
const handleRename = async () => {
- const trimmed = renameValue.trim();
- if (!trimmed || trimmed === file.name) {
- setRenameValue(file.name);
+ let trimmed = renameValue.trim();
+ if (!trimmed) {
+ setRenameValue(baseName);
setIsRenaming(false);
return;
}
+
+ // Auto-append extension if the user didn't type it
+ if (!trimmed.toLowerCase().endsWith(ext.toLowerCase())) {
+ trimmed += ext;
+ }
+
+ if (trimmed === file.name) {
+ setRenameValue(baseName);
+ setIsRenaming(false);
+ return;
+ }
+
try {
const res = await api.rename(file.path, trimmed);
// Build new path from the response or derive it
@@ -44,7 +61,7 @@ export default function OnlyOfficeEditor({ file, type, onClose, onRename }: Only
setIsRenaming(false);
} catch (err) {
console.error('Failed to rename', err);
- setRenameValue(file.name);
+ setRenameValue(baseName);
setIsRenaming(false);
}
};
@@ -139,28 +156,27 @@ export default function OnlyOfficeEditor({ file, type, onClose, onRename }: Only
onBlur={handleRename}
onKeyDown={(e) => {
if (e.key === 'Enter') handleRename();
- if (e.key === 'Escape') { setRenameValue(file.name); setIsRenaming(false); }
+ if (e.key === 'Escape') { setRenameValue(baseName); setIsRenaming(false); }
}}
- className="font-medium text-sm text-gray-700 bg-white border border-blue-400 rounded px-2 py-0.5 outline-none min-w-[200px]"
+ className="font-medium text-sm text-gray-700 bg-white border border-blue-400 rounded px-2 py-0.5 outline-none w-1/3 min-w-[300px] max-w-[600px]"
autoFocus
/>
) : (
{
setIsRenaming(true);
- setRenameValue(file.name);
+ setRenameValue(baseName);
setTimeout(() => {
if (renameInputRef.current) {
renameInputRef.current.focus();
- const dotIdx = file.name.lastIndexOf('.');
- renameInputRef.current.setSelectionRange(0, dotIdx > 0 ? dotIdx : file.name.length);
+ renameInputRef.current.select();
}
}, 50);
}}
- className="font-medium text-sm text-gray-700 hover:text-blue-600 hover:underline cursor-pointer transition-colors"
+ className="font-medium text-sm text-gray-700 hover:text-blue-600 hover:underline cursor-pointer transition-colors truncate max-w-[500px]"
title="Click to rename"
>
- {file.name}
+ {baseName}
)}