From 49895eb16653d50ce81706353d12dbf6adea19f3 Mon Sep 17 00:00:00 2001 From: alma Date: Sun, 20 Apr 2025 17:12:26 +0200 Subject: [PATCH] carnet panel3 --- app/api/nextcloud/files/[id]/route.ts | 112 +++-------- app/carnet/page.tsx | 274 +++++++++++++++++++------- components/carnet/editor.tsx | 167 ++++++++-------- components/carnet/file-list.tsx | 71 ------- 4 files changed, 320 insertions(+), 304 deletions(-) delete mode 100644 components/carnet/file-list.tsx diff --git a/app/api/nextcloud/files/[id]/route.ts b/app/api/nextcloud/files/[id]/route.ts index d9404929..37cb69e1 100644 --- a/app/api/nextcloud/files/[id]/route.ts +++ b/app/api/nextcloud/files/[id]/route.ts @@ -12,30 +12,6 @@ declare global { const prisma = global.prisma || new PrismaClient(); if (process.env.NODE_ENV !== 'production') global.prisma = prisma; -// Helper function to create WebDAV client -const createWebDAVClient = async (userId: string) => { - const credentials = await prisma.webDAVCredentials.findUnique({ - where: { userId }, - }); - - if (!credentials) { - throw new Error('No WebDAV credentials found'); - } - - const baseURL = process.env.NEXTCLOUD_URL; - if (!baseURL) { - throw new Error('NEXTCLOUD_URL environment variable is not set'); - } - - const normalizedBaseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL; - - return createClient(`${normalizedBaseURL}/remote.php/dav`, { - username: credentials.username, - password: credentials.password, - authType: 'password', - }); -}; - export async function GET( request: Request, { params }: { params: { id: string } } @@ -46,9 +22,33 @@ export async function GET( return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const client = await createWebDAVClient(session.user.id); + // Get WebDAV credentials + const credentials = await prisma.webDAVCredentials.findUnique({ + where: { userId: session.user.id }, + }); + + if (!credentials) { + console.error('No WebDAV credentials found for user:', session.user.id); + return NextResponse.json({ error: 'No WebDAV credentials found' }, { status: 404 }); + } + + // Initialize WebDAV client + const baseURL = process.env.NEXTCLOUD_URL; + if (!baseURL) { + throw new Error('NEXTCLOUD_URL environment variable is not set'); + } + + // Remove trailing slash if present + const normalizedBaseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL; + + const client = createClient(`${normalizedBaseURL}/remote.php/dav`, { + username: credentials.username, + password: credentials.password, + authType: 'password', + }); try { + // Get the file content const content = await client.getFileContents(params.id); const textContent = content.toString('utf-8'); @@ -69,66 +69,4 @@ export async function GET( } return NextResponse.json({ error: 'Failed to fetch file' }, { status: 500 }); } -} - -export async function POST( - request: Request, - { params }: { params: { id: string } } -) { - try { - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const { content } = await request.json(); - const client = await createWebDAVClient(session.user.id); - - try { - await client.putFileContents(params.id, content); - return NextResponse.json({ success: true }); - } catch (error) { - console.error('Error saving file content:', error); - return NextResponse.json({ error: 'Failed to save file content' }, { status: 500 }); - } - } catch (error) { - console.error('Error saving file:', error); - return NextResponse.json({ error: 'Failed to save file' }, { status: 500 }); - } -} - -export async function PUT( - request: Request, - { params }: { params: { id: string } } -) { - try { - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const { title, content, folder } = await request.json(); - const client = await createWebDAVClient(session.user.id); - - try { - const path = `/files/${client.credentials.username}/Private/${folder}/${title}.md`; - await client.putFileContents(path, content); - return NextResponse.json({ - success: true, - id: path, - title, - lastModified: new Date().toISOString(), - size: content.length, - type: 'file', - mime: 'text/markdown', - etag: '' - }); - } catch (error) { - console.error('Error creating file:', error); - return NextResponse.json({ error: 'Failed to create file' }, { status: 500 }); - } - } catch (error) { - console.error('Error creating file:', error); - return NextResponse.json({ error: 'Failed to create file' }, { status: 500 }); - } } \ No newline at end of file diff --git a/app/carnet/page.tsx b/app/carnet/page.tsx index 4f50c138..4b236791 100644 --- a/app/carnet/page.tsx +++ b/app/carnet/page.tsx @@ -1,98 +1,234 @@ "use client"; -import React, { useState, useEffect } from 'react'; -import { FileList } from '@/components/carnet/file-list'; -import { Editor } from '@/components/carnet/editor'; -import { Plus } from 'lucide-react'; +import { useEffect, useState, useRef } from "react"; +import { useSession } from "next-auth/react"; +import { redirect } from "next/navigation"; +import Navigation from "@/components/carnet/navigation"; +import { NotesView } from "@/components/carnet/notes-view"; +import { Editor } from "@/components/carnet/editor"; +import { PanelResizer } from "@/components/carnet/panel-resizer"; +import { useMediaQuery } from "@/hooks/use-media-query"; + +// Layout modes +export enum PaneLayout { + TagSelection = "tag-selection", + ItemSelection = "item-selection", + TableView = "table-view", + Editing = "editing" +} interface Note { id: string; title: string; - lastModified: string; - size: number; - type: string; - mime: string; - etag: string; - content?: string; + content: string; + lastEdited: Date; } export default function CarnetPage() { - const [notes, setNotes] = useState([]); + const { data: session, status } = useSession(); + const [isLoading, setIsLoading] = useState(true); + const [layoutMode, setLayoutMode] = useState(PaneLayout.ItemSelection); const [selectedNote, setSelectedNote] = useState(null); - const [currentFolder, setCurrentFolder] = useState('Notes'); - const [loading, setLoading] = useState(true); + const [isMobile, setIsMobile] = useState(false); + const [showNav, setShowNav] = useState(true); + const [showNotes, setShowNotes] = useState(true); + const [nextcloudFolders, setNextcloudFolders] = useState([]); + const [selectedFolder, setSelectedFolder] = useState('Notes'); + + // Panel widths state + const [navWidth, setNavWidth] = useState(220); + const [notesWidth, setNotesWidth] = useState(400); + const [isDraggingNav, setIsDraggingNav] = useState(false); + const [isDraggingNotes, setIsDraggingNotes] = useState(false); + + // Check screen size + const isSmallScreen = useMediaQuery("(max-width: 768px)"); + const isMediumScreen = useMediaQuery("(max-width: 1024px)"); + + // Cache for Nextcloud folders + const foldersCache = useRef<{ folders: string[]; timestamp: number } | null>(null); useEffect(() => { - fetchNotes(); - }, [currentFolder]); - - const fetchNotes = async () => { - try { - setLoading(true); - const response = await fetch(`/api/nextcloud/files?folder=${encodeURIComponent(currentFolder)}`); - if (!response.ok) { - throw new Error('Failed to fetch notes'); + const fetchNextcloudFolders = async () => { + // Check cache first + if (foldersCache.current) { + const cacheAge = Date.now() - foldersCache.current.timestamp; + if (cacheAge < 5 * 60 * 1000) { // 5 minutes cache + setNextcloudFolders(foldersCache.current.folders); + return; + } } - const data = await response.json(); - setNotes(data); - } catch (err) { - console.error('Error fetching notes:', err); - } finally { - setLoading(false); + + try { + const response = await fetch('/api/nextcloud/status'); + if (!response.ok) { + throw new Error('Failed to fetch Nextcloud folders'); + } + const data = await response.json(); + const folders = data.folders || []; + + // Update cache + foldersCache.current = { + folders, + timestamp: Date.now() + }; + + setNextcloudFolders(folders); + } catch (err) { + console.error('Error fetching Nextcloud folders:', err); + setNextcloudFolders([]); + } + }; + + if (status === "authenticated") { + fetchNextcloudFolders(); + } + }, [status]); + + useEffect(() => { + if (status === "unauthenticated") { + redirect("/signin"); + } + if (status !== "loading") { + setIsLoading(false); + } + }, [status]); + + useEffect(() => { + if (isSmallScreen) { + setIsMobile(true); + setShowNav(false); + setShowNotes(false); + } else if (isMediumScreen) { + setIsMobile(false); + setShowNav(true); + setShowNotes(false); + } else { + setIsMobile(false); + setShowNav(true); + setShowNotes(true); + } + }, [isSmallScreen, isMediumScreen]); + + // Handle panel resizing + const handleNavResize = (e: MouseEvent) => { + if (!isDraggingNav) return; + const newWidth = e.clientX; + if (newWidth >= 48 && newWidth <= 400) { + setNavWidth(newWidth); + } + }; + + const handleNotesResize = (e: MouseEvent) => { + if (!isDraggingNotes) return; + const newWidth = e.clientX - navWidth - 2; // 2px for the resizer + if (newWidth >= 200) { + setNotesWidth(newWidth); } }; const handleNoteSelect = (note: Note) => { setSelectedNote(note); + if (isMobile) { + setShowNotes(false); + } }; - const handleNoteSave = (updatedNote: Note) => { - setNotes(prevNotes => { - const index = prevNotes.findIndex(n => n.id === updatedNote.id); - if (index === -1) { - return [...prevNotes, updatedNote]; - } - const newNotes = [...prevNotes]; - newNotes[index] = updatedNote; - return newNotes; - }); - setSelectedNote(updatedNote); + const handleNoteSave = (note: Note) => { + // TODO: Implement note saving logic + console.log('Saving note:', note); }; - const handleNewNote = () => { - setSelectedNote(null); + const handleFolderSelect = (folder: string) => { + console.log('Selected folder:', folder); + setSelectedFolder(folder); + setLayoutMode(PaneLayout.ItemSelection); }; + if (isLoading) { + return ( +
+
+
+ ); + } + return ( -
- {/* Sidebar */} -
-
- -
- -
+
+
+
+ {/* Navigation Panel */} + {showNav && ( + <> +
+ +
- {/* Editor */} -
- + {/* Navigation Resizer */} + setIsDraggingNav(true)} + onDragEnd={() => setIsDraggingNav(false)} + onDrag={handleNavResize} + /> + + )} + + {/* Notes Panel */} + {showNotes && ( + <> +
+ +
+ + {/* Notes Resizer */} + setIsDraggingNotes(true)} + onDragEnd={() => setIsDraggingNotes(false)} + onDrag={handleNotesResize} + /> + + )} + + {/* Editor Panel */} +
+ +
+ + {/* Mobile Navigation Toggle */} + {isMobile && ( +
+ + +
+ )} +
-
+
); } \ No newline at end of file diff --git a/components/carnet/editor.tsx b/components/carnet/editor.tsx index 84a82fad..95bcb257 100644 --- a/components/carnet/editor.tsx +++ b/components/carnet/editor.tsx @@ -1,12 +1,7 @@ "use client"; import React, { useState, useEffect } from 'react'; -import { Save } from 'lucide-react'; -import dynamic from 'next/dynamic'; - -// Dynamically import the editor to avoid SSR issues -const ReactQuill = dynamic(() => import('react-quill'), { ssr: false }); -import 'react-quill/dist/quill.snow.css'; +import { Image, FileText, Link, List } from 'lucide-react'; interface Note { id: string; @@ -20,92 +15,110 @@ interface Note { } interface EditorProps { - note: Note | null; - onSave: (note: Note) => void; - currentFolder: string; + note?: Note | null; + onSave?: (note: Note) => void; } -export function Editor({ note, onSave, currentFolder }: EditorProps) { - const [content, setContent] = useState(''); - const [isSaving, setIsSaving] = useState(false); - const [isLoading, setIsLoading] = useState(false); +export const Editor: React.FC = ({ note, onSave }) => { + const [title, setTitle] = useState(note?.title || ''); + const [content, setContent] = useState(note?.content || ''); + const [loading, setLoading] = useState(false); useEffect(() => { - if (note?.content) { - setIsLoading(true); - setContent(note.content); - setIsLoading(false); + const fetchNoteContent = async () => { + if (note?.id) { + try { + setLoading(true); + const response = await fetch(`/api/nextcloud/files/${encodeURIComponent(note.id)}`); + if (!response.ok) { + throw new Error('Failed to fetch note content'); + } + const data = await response.json(); + setContent(data.content || ''); + } catch (err) { + console.error('Error fetching note content:', err); + } finally { + setLoading(false); + } + } + }; + + if (note) { + setTitle(note.title); + fetchNoteContent(); } else { + setTitle(''); setContent(''); } }, [note]); - const handleSave = async () => { - if (!note) return; - - setIsSaving(true); - try { - const updatedNote = { + const handleTitleChange = (e: React.ChangeEvent) => { + setTitle(e.target.value); + }; + + const handleContentChange = (e: React.ChangeEvent) => { + setContent(e.target.value); + }; + + const handleSave = () => { + if (note?.id) { + onSave?.({ ...note, - content, - lastModified: new Date().toISOString(), - size: content.length - }; - onSave(updatedNote); - } catch (error) { - console.error('Failed to save note:', error); - } finally { - setIsSaving(false); + title, + content + }); } }; - if (isLoading) { - return ( -
-
-
- ); - } - - if (!note) { - return ( -
-

Select a note to edit

-
- ); - } - return ( -
-
-

{note.title}

- -
- -
- + {/* Title Bar */} +
+
+ + {/* Toolbar */} +
+
+ + + + +
+
+ + {/* Editor Area */} +
+ {loading ? ( +
+
+
+ ) : ( +