diff --git a/app/api/nextcloud/files/[id]/route.ts b/app/api/nextcloud/files/[id]/route.ts index 37cb69e1..d9404929 100644 --- a/app/api/nextcloud/files/[id]/route.ts +++ b/app/api/nextcloud/files/[id]/route.ts @@ -12,6 +12,30 @@ 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 } } @@ -22,33 +46,9 @@ export async function GET( return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - // 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', - }); + const client = await createWebDAVClient(session.user.id); try { - // Get the file content const content = await client.getFileContents(params.id); const textContent = content.toString('utf-8'); @@ -69,4 +69,66 @@ 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 4b236791..4f50c138 100644 --- a/app/carnet/page.tsx +++ b/app/carnet/page.tsx @@ -1,234 +1,98 @@ "use client"; -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" -} +import React, { useState, useEffect } from 'react'; +import { FileList } from '@/components/carnet/file-list'; +import { Editor } from '@/components/carnet/editor'; +import { Plus } from 'lucide-react'; interface Note { id: string; title: string; - content: string; - lastEdited: Date; + lastModified: string; + size: number; + type: string; + mime: string; + etag: string; + content?: string; } export default function CarnetPage() { - const { data: session, status } = useSession(); - const [isLoading, setIsLoading] = useState(true); - const [layoutMode, setLayoutMode] = useState(PaneLayout.ItemSelection); + const [notes, setNotes] = useState([]); const [selectedNote, setSelectedNote] = useState(null); - 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); + const [currentFolder, setCurrentFolder] = useState('Notes'); + const [loading, setLoading] = useState(true); useEffect(() => { - 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; - } + 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'); } - - 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 data = await response.json(); + setNotes(data); + } catch (err) { + console.error('Error fetching notes:', err); + } finally { + setLoading(false); } }; const handleNoteSelect = (note: Note) => { setSelectedNote(note); - if (isMobile) { - setShowNotes(false); - } }; - const handleNoteSave = (note: Note) => { - // TODO: Implement note saving logic - console.log('Saving note:', note); + 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 handleFolderSelect = (folder: string) => { - console.log('Selected folder:', folder); - setSelectedFolder(folder); - setLayoutMode(PaneLayout.ItemSelection); + const handleNewNote = () => { + setSelectedNote(null); }; - if (isLoading) { - return ( -
-
-
- ); - } - return ( -
-
-
- {/* Navigation Panel */} - {showNav && ( - <> -
- -
- - {/* 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 && ( -
- - -
- )} +
+ {/* Sidebar */} +
+
+
+
-
+ + {/* Editor */} +
+ +
+ ); } \ No newline at end of file diff --git a/components/carnet/editor.tsx b/components/carnet/editor.tsx index 95bcb257..84a82fad 100644 --- a/components/carnet/editor.tsx +++ b/components/carnet/editor.tsx @@ -1,7 +1,12 @@ "use client"; import React, { useState, useEffect } from 'react'; -import { Image, FileText, Link, List } from 'lucide-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'; interface Note { id: string; @@ -15,110 +20,92 @@ interface Note { } interface EditorProps { - note?: Note | null; - onSave?: (note: Note) => void; + note: Note | null; + onSave: (note: Note) => void; + currentFolder: string; } -export const Editor: React.FC = ({ note, onSave }) => { - const [title, setTitle] = useState(note?.title || ''); - const [content, setContent] = useState(note?.content || ''); - const [loading, setLoading] = useState(false); +export function Editor({ note, onSave, currentFolder }: EditorProps) { + const [content, setContent] = useState(''); + const [isSaving, setIsSaving] = useState(false); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { - 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(); + if (note?.content) { + setIsLoading(true); + setContent(note.content); + setIsLoading(false); } else { - setTitle(''); setContent(''); } }, [note]); - const handleTitleChange = (e: React.ChangeEvent) => { - setTitle(e.target.value); - }; - - const handleContentChange = (e: React.ChangeEvent) => { - setContent(e.target.value); - }; - - const handleSave = () => { - if (note?.id) { - onSave?.({ + const handleSave = async () => { + if (!note) return; + + setIsSaving(true); + try { + const updatedNote = { ...note, - title, - content - }); + content, + lastModified: new Date().toISOString(), + size: content.length + }; + onSave(updatedNote); + } catch (error) { + console.error('Failed to save note:', error); + } finally { + setIsSaving(false); } }; + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!note) { + return ( +
+

Select a note to edit

+
+ ); + } + return ( -
- {/* Title Bar */} -
- +
+

{note.title}

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