carnet panel3
This commit is contained in:
parent
2bb0fb74a1
commit
dea5ae1ff8
@ -12,6 +12,34 @@ declare global {
|
|||||||
const prisma = global.prisma || new PrismaClient();
|
const prisma = global.prisma || new PrismaClient();
|
||||||
if (process.env.NODE_ENV !== 'production') global.prisma = prisma;
|
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;
|
||||||
|
const webdavURL = `${normalizedBaseURL}/remote.php/dav`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
client: createClient(webdavURL, {
|
||||||
|
username: credentials.username,
|
||||||
|
password: credentials.password,
|
||||||
|
authType: 'password',
|
||||||
|
}),
|
||||||
|
username: credentials.username
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
@ -22,84 +50,118 @@ export async function GET(request: Request) {
|
|||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const folder = searchParams.get('folder') || 'Notes';
|
const folder = searchParams.get('folder') || 'Notes';
|
||||||
|
|
||||||
// Get WebDAV credentials
|
const { client, username } = await createWebDAVClient(session.user.id);
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Using credentials for user:', credentials.username);
|
|
||||||
console.log('Accessing folder:', folder);
|
|
||||||
|
|
||||||
// Initialize WebDAV client with proper URL construction
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Construct the full WebDAV URL
|
|
||||||
const webdavURL = `${normalizedBaseURL}/remote.php/dav`;
|
|
||||||
console.log('WebDAV base URL:', webdavURL);
|
|
||||||
|
|
||||||
const client = createClient(webdavURL, {
|
|
||||||
username: credentials.username,
|
|
||||||
password: credentials.password,
|
|
||||||
authType: 'password',
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// List files in the specified folder
|
const path = `/files/${username}/Private/${folder}`;
|
||||||
const path = `/files/${credentials.username}/Private/${folder}`;
|
|
||||||
console.log('Fetching contents from path:', path);
|
console.log('Fetching contents from path:', path);
|
||||||
|
|
||||||
const files = await client.getDirectoryContents(path);
|
const files = await client.getDirectoryContents(path);
|
||||||
console.log('Raw files response:', JSON.stringify(files, null, 2));
|
console.log('Raw files response:', JSON.stringify(files, null, 2));
|
||||||
|
|
||||||
// Filter for .md files and format the response
|
|
||||||
const markdownFiles = files
|
const markdownFiles = files
|
||||||
.filter((file: any) => {
|
.filter((file: any) => file.basename.endsWith('.md'))
|
||||||
const isMarkdown = file.basename.endsWith('.md');
|
.map((file: any) => ({
|
||||||
console.log(`File: ${file.basename}, isMarkdown: ${isMarkdown}`);
|
id: file.filename,
|
||||||
return isMarkdown;
|
title: file.basename.replace('.md', ''),
|
||||||
})
|
lastModified: new Date(file.lastmod).toISOString(),
|
||||||
.map((file: any) => {
|
size: file.size,
|
||||||
const fileData = {
|
type: 'file',
|
||||||
id: file.filename,
|
mime: file.mime,
|
||||||
title: file.basename.replace('.md', ''),
|
etag: file.etag
|
||||||
lastModified: new Date(file.lastmod).toISOString(),
|
}));
|
||||||
size: file.size,
|
|
||||||
type: 'file',
|
|
||||||
mime: file.mime,
|
|
||||||
etag: file.etag
|
|
||||||
};
|
|
||||||
console.log('Formatted file data:', JSON.stringify(fileData, null, 2));
|
|
||||||
return fileData;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Found markdown files:', markdownFiles.length);
|
|
||||||
console.log('Final response:', JSON.stringify(markdownFiles, null, 2));
|
|
||||||
return NextResponse.json(markdownFiles);
|
return NextResponse.json(markdownFiles);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error listing directory contents:', error);
|
console.error('Error listing directory contents:', error);
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error('Error details:', error.message);
|
|
||||||
console.error('Error stack:', error.stack);
|
|
||||||
}
|
|
||||||
return NextResponse.json({ error: 'Failed to list directory contents' }, { status: 500 });
|
return NextResponse.json({ error: 'Failed to list directory contents' }, { status: 500 });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching files:', error);
|
console.error('Error fetching files:', error);
|
||||||
if (error instanceof Error) {
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||||
console.error('Error details:', error.message);
|
}
|
||||||
console.error('Error stack:', error.stack);
|
}
|
||||||
}
|
|
||||||
return NextResponse.json({ error: 'Failed to fetch files' }, { status: 500 });
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, content, folder } = await request.json();
|
||||||
|
if (!title || !content || !folder) {
|
||||||
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { client, username } = await createWebDAVClient(session.user.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const path = `/files/${username}/Private/${folder}/${title}.md`;
|
||||||
|
console.log('Saving note to path:', path);
|
||||||
|
|
||||||
|
await client.putFileContents(path, content);
|
||||||
|
|
||||||
|
// Get the file details after saving
|
||||||
|
const fileDetails = await client.stat(path);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
id: fileDetails.filename,
|
||||||
|
title: fileDetails.basename.replace('.md', ''),
|
||||||
|
lastModified: new Date(fileDetails.lastmod).toISOString(),
|
||||||
|
size: fileDetails.size,
|
||||||
|
type: 'file',
|
||||||
|
mime: fileDetails.mime,
|
||||||
|
etag: fileDetails.etag
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving note:', error);
|
||||||
|
return NextResponse.json({ error: 'Failed to save note' }, { status: 500 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in POST request:', error);
|
||||||
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(request: Request) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, title, content, folder } = await request.json();
|
||||||
|
if (!id || !title || !content || !folder) {
|
||||||
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { client, username } = await createWebDAVClient(session.user.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const path = `/files/${username}/Private/${folder}/${title}.md`;
|
||||||
|
console.log('Updating note at path:', path);
|
||||||
|
|
||||||
|
await client.putFileContents(path, content);
|
||||||
|
|
||||||
|
// Get the updated file details
|
||||||
|
const fileDetails = await client.stat(path);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
id: fileDetails.filename,
|
||||||
|
title: fileDetails.basename.replace('.md', ''),
|
||||||
|
lastModified: new Date(fileDetails.lastmod).toISOString(),
|
||||||
|
size: fileDetails.size,
|
||||||
|
type: 'file',
|
||||||
|
mime: fileDetails.mime,
|
||||||
|
etag: fileDetails.etag
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating note:', error);
|
||||||
|
return NextResponse.json({ error: 'Failed to update note' }, { status: 500 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in PUT request:', error);
|
||||||
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,10 +185,7 @@ export default function CarnetPage() {
|
|||||||
{/* Notes Panel */}
|
{/* Notes Panel */}
|
||||||
{showNotes && (
|
{showNotes && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className="flex-1 overflow-hidden">
|
||||||
className="flex flex-col h-full bg-carnet-bg"
|
|
||||||
style={{ width: `${notesWidth}px` }}
|
|
||||||
>
|
|
||||||
<NotesView
|
<NotesView
|
||||||
onNoteSelect={handleNoteSelect}
|
onNoteSelect={handleNoteSelect}
|
||||||
currentFolder={selectedFolder}
|
currentFolder={selectedFolder}
|
||||||
@ -206,8 +203,12 @@ export default function CarnetPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Editor Panel */}
|
{/* Editor Panel */}
|
||||||
<div className="flex-1 flex flex-col h-full bg-carnet-bg">
|
<div className="flex-1 overflow-hidden">
|
||||||
<Editor note={selectedNote} onSave={handleNoteSave} />
|
<Editor
|
||||||
|
note={selectedNote}
|
||||||
|
onSave={handleNoteSave}
|
||||||
|
currentFolder={selectedFolder}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Navigation Toggle */}
|
{/* Mobile Navigation Toggle */}
|
||||||
|
|||||||
@ -15,11 +15,13 @@ interface Note {
|
|||||||
interface EditorProps {
|
interface EditorProps {
|
||||||
note?: Note | null;
|
note?: Note | null;
|
||||||
onSave?: (note: Note) => void;
|
onSave?: (note: Note) => void;
|
||||||
|
currentFolder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editor: React.FC<EditorProps> = ({ note, onSave }) => {
|
export const Editor: React.FC<EditorProps> = ({ note, onSave, currentFolder = 'Notes' }) => {
|
||||||
const [title, setTitle] = useState(note?.title || '');
|
const [title, setTitle] = useState(note?.title || '');
|
||||||
const [content, setContent] = useState(note?.content || '');
|
const [content, setContent] = useState(note?.content || '');
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (note) {
|
if (note) {
|
||||||
@ -36,16 +38,45 @@ export const Editor: React.FC<EditorProps> = ({ note, onSave }) => {
|
|||||||
setContent(e.target.value);
|
setContent(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = async () => {
|
||||||
if (note?.id) {
|
if (!title || !content) return;
|
||||||
onSave?.({
|
|
||||||
id: note.id,
|
setIsSaving(true);
|
||||||
title,
|
try {
|
||||||
content,
|
const endpoint = note?.id ? '/api/nextcloud/files' : '/api/nextcloud/files';
|
||||||
lastEdited: new Date(),
|
const method = note?.id ? 'PUT' : 'POST';
|
||||||
category: note.category,
|
|
||||||
tags: note.tags
|
const response = await fetch(endpoint, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: note?.id,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
folder: currentFolder
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save note');
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedNote = await response.json();
|
||||||
|
onSave?.({
|
||||||
|
id: savedNote.id,
|
||||||
|
title: savedNote.title,
|
||||||
|
content,
|
||||||
|
lastEdited: new Date(savedNote.lastModified),
|
||||||
|
category: note?.category,
|
||||||
|
tags: note?.tags
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving note:', error);
|
||||||
|
// TODO: Show error message to user
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -75,8 +106,9 @@ export const Editor: React.FC<EditorProps> = ({ note, onSave }) => {
|
|||||||
<Image className="h-4 w-4 text-carnet-text-muted" />
|
<Image className="h-4 w-4 text-carnet-text-muted" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="p-1.5 rounded hover:bg-carnet-hover"
|
className={`p-1.5 rounded hover:bg-carnet-hover ${isSaving ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
|
disabled={isSaving}
|
||||||
>
|
>
|
||||||
<FileText className="h-4 w-4 text-carnet-text-muted" />
|
<FileText className="h-4 w-4 text-carnet-text-muted" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user