carnet
This commit is contained in:
parent
bcbd4866df
commit
56c3f5a0ba
@ -3,10 +3,45 @@
|
||||
import { useEffect, useState } 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;
|
||||
content: string;
|
||||
lastEdited: Date;
|
||||
}
|
||||
|
||||
export default function CarnetPage() {
|
||||
const { data: session, status } = useSession();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [layoutMode, setLayoutMode] = useState<PaneLayout>(PaneLayout.ItemSelection);
|
||||
const [selectedNote, setSelectedNote] = useState<Note | null>(null);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [showNav, setShowNav] = useState(true);
|
||||
const [showNotes, setShowNotes] = useState(true);
|
||||
|
||||
// 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)");
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "unauthenticated") {
|
||||
@ -17,6 +52,51 @@ export default function CarnetPage() {
|
||||
}
|
||||
}, [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 = (note: Note) => {
|
||||
// TODO: Implement note saving logic
|
||||
console.log('Saving note:', note);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
@ -26,14 +106,69 @@ export default function CarnetPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="w-full h-screen bg-black">
|
||||
<div className="w-full h-full px-4 pt-12 pb-4">
|
||||
<iframe
|
||||
src={process.env.NEXT_PUBLIC_IFRAME_CARNET_URL}
|
||||
className="w-full h-full border-0"
|
||||
title="Carnet"
|
||||
/>
|
||||
<main className="flex h-screen bg-background">
|
||||
{/* Navigation Panel */}
|
||||
{showNav && (
|
||||
<>
|
||||
<div
|
||||
className="flex flex-col h-full bg-sidebar"
|
||||
style={{ width: `${navWidth}px` }}
|
||||
>
|
||||
<Navigation onLayoutChange={setLayoutMode} />
|
||||
</div>
|
||||
|
||||
{/* Navigation Resizer */}
|
||||
<PanelResizer
|
||||
isDragging={isDraggingNav}
|
||||
onDragStart={() => setIsDraggingNav(true)}
|
||||
onDragEnd={() => setIsDraggingNav(false)}
|
||||
onDrag={handleNavResize}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Notes Panel */}
|
||||
{showNotes && (
|
||||
<>
|
||||
<div
|
||||
className="flex flex-col h-full bg-panel"
|
||||
style={{ width: `${notesWidth}px` }}
|
||||
>
|
||||
<NotesView onNoteSelect={handleNoteSelect} />
|
||||
</div>
|
||||
|
||||
{/* Notes Resizer */}
|
||||
<PanelResizer
|
||||
isDragging={isDraggingNotes}
|
||||
onDragStart={() => setIsDraggingNotes(true)}
|
||||
onDragEnd={() => setIsDraggingNotes(false)}
|
||||
onDrag={handleNotesResize}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Editor Panel */}
|
||||
<div className="flex-1 flex flex-col h-full bg-background">
|
||||
<Editor note={selectedNote} onSave={handleNoteSave} />
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation Toggle */}
|
||||
{isMobile && (
|
||||
<div className="fixed bottom-4 right-4 flex space-x-2">
|
||||
<button
|
||||
className="p-2 rounded-full bg-primary text-white"
|
||||
onClick={() => setShowNav(!showNav)}
|
||||
>
|
||||
{showNav ? 'Hide Nav' : 'Show Nav'}
|
||||
</button>
|
||||
<button
|
||||
className="p-2 rounded-full bg-primary text-white"
|
||||
onClick={() => setShowNotes(!showNotes)}
|
||||
>
|
||||
{showNotes ? 'Hide Notes' : 'Show Notes'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
83
components/carnet/editor.tsx
Normal file
83
components/carnet/editor.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Image, FileText } from 'lucide-react';
|
||||
|
||||
interface EditorProps {
|
||||
note?: {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
};
|
||||
onSave?: (note: { id: string; title: string; content: string }) => void;
|
||||
}
|
||||
|
||||
export const Editor: React.FC<EditorProps> = ({ note, onSave }) => {
|
||||
const [title, setTitle] = useState(note?.title || '');
|
||||
const [content, setContent] = useState(note?.content || '');
|
||||
|
||||
useEffect(() => {
|
||||
if (note) {
|
||||
setTitle(note.title);
|
||||
setContent(note.content);
|
||||
}
|
||||
}, [note]);
|
||||
|
||||
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(e.target.value);
|
||||
};
|
||||
|
||||
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setContent(e.target.value);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (note?.id) {
|
||||
onSave?.({
|
||||
id: note.id,
|
||||
title,
|
||||
content
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Title Bar */}
|
||||
<div className="p-4 border-b">
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
placeholder="Note title"
|
||||
className="w-full text-xl font-semibold focus:outline-none bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Editor Area */}
|
||||
<div className="flex-1 p-4">
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={handleContentChange}
|
||||
placeholder="Start writing..."
|
||||
className="w-full h-full resize-none focus:outline-none bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="p-4 border-t">
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700"
|
||||
onClick={handleSave}
|
||||
>
|
||||
<FileText className="h-5 w-5" />
|
||||
</button>
|
||||
<button className="p-2 text-gray-500 hover:text-gray-700">
|
||||
<Image className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
47
components/carnet/navigation.tsx
Normal file
47
components/carnet/navigation.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { BookOpen, Tag, Trash2 } from 'lucide-react';
|
||||
import { PaneLayout } from '@/app/carnet/page';
|
||||
|
||||
interface NavigationProps {
|
||||
onLayoutChange?: (layout: PaneLayout) => void;
|
||||
}
|
||||
|
||||
export const Navigation: React.FC<NavigationProps> = ({ onLayoutChange }) => {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||
<h2 className="text-menu-item font-medium text-text">Navigation</h2>
|
||||
</div>
|
||||
|
||||
{/* Navigation Items */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div
|
||||
className="flex items-center p-3 hover:bg-contrast cursor-pointer"
|
||||
onClick={() => onLayoutChange?.(PaneLayout.ItemSelection)}
|
||||
>
|
||||
<BookOpen className="w-5 h-5 mr-3 text-passive-1" />
|
||||
<h3 className="text-menu-item font-medium text-text">All Notes</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center p-3 hover:bg-contrast cursor-pointer"
|
||||
onClick={() => onLayoutChange?.(PaneLayout.TagSelection)}
|
||||
>
|
||||
<Tag className="w-5 h-5 mr-3 text-passive-1" />
|
||||
<h3 className="text-menu-item font-medium text-text">Tags</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center p-3 hover:bg-contrast cursor-pointer"
|
||||
onClick={() => onLayoutChange?.(PaneLayout.TableView)}
|
||||
>
|
||||
<Trash2 className="w-5 h-5 mr-3 text-passive-1" />
|
||||
<h3 className="text-menu-item font-medium text-text">Trash</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
69
components/carnet/notes-view.tsx
Normal file
69
components/carnet/notes-view.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
interface Note {
|
||||
id: string;
|
||||
title: string;
|
||||
lastEdited: Date;
|
||||
}
|
||||
|
||||
interface NotesViewProps {
|
||||
onNoteSelect?: (note: Note) => void;
|
||||
}
|
||||
|
||||
export const NotesView: React.FC<NotesViewProps> = ({ onNoteSelect }) => {
|
||||
const [notes, setNotes] = useState<Note[]>([
|
||||
{
|
||||
id: '1',
|
||||
title: 'Sample Note',
|
||||
lastEdited: new Date(Date.now() - 2 * 60 * 60 * 1000) // 2 hours ago
|
||||
}
|
||||
]);
|
||||
|
||||
const handleNewNote = () => {
|
||||
const newNote: Note = {
|
||||
id: Date.now().toString(),
|
||||
title: 'New Note',
|
||||
lastEdited: new Date()
|
||||
};
|
||||
setNotes([newNote, ...notes]);
|
||||
onNoteSelect?.(newNote);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-border">
|
||||
<h2 className="text-lg font-medium text-foreground">Notes</h2>
|
||||
<button
|
||||
className="p-2 rounded-full hover:bg-contrast"
|
||||
onClick={handleNewNote}
|
||||
>
|
||||
<Plus className="w-5 h-5 text-passive-1" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Notes List */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{notes.map((note) => (
|
||||
<div
|
||||
key={note.id}
|
||||
className="p-4 hover:bg-contrast cursor-pointer"
|
||||
onClick={() => onNoteSelect?.(note)}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-menu-item font-medium text-foreground">
|
||||
{note.title}
|
||||
</span>
|
||||
<span className="text-xs text-passive-1">
|
||||
Last edited {note.lastEdited.toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
33
components/carnet/panel-resizer.tsx
Normal file
33
components/carnet/panel-resizer.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface PanelResizerProps {
|
||||
isDragging: boolean;
|
||||
onDragStart: () => void;
|
||||
onDragEnd: () => void;
|
||||
onDrag: (e: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export const PanelResizer: React.FC<PanelResizerProps> = ({
|
||||
isDragging,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onDrag
|
||||
}) => {
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
onDragStart();
|
||||
document.addEventListener('mousemove', onDrag);
|
||||
document.addEventListener('mouseup', onDragEnd);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-1 bg-border cursor-col-resize hover:bg-info transition-colors ${
|
||||
isDragging ? 'bg-info' : ''
|
||||
}`}
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
);
|
||||
};
|
||||
25
hooks/use-media-query.ts
Normal file
25
hooks/use-media-query.ts
Normal file
@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useMediaQuery(query: string): boolean {
|
||||
const [matches, setMatches] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(query);
|
||||
|
||||
// Set initial value
|
||||
setMatches(media.matches);
|
||||
|
||||
// Create event listener
|
||||
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
|
||||
|
||||
// Add the listener
|
||||
media.addEventListener('change', listener);
|
||||
|
||||
// Clean up
|
||||
return () => media.removeEventListener('change', listener);
|
||||
}, [query]);
|
||||
|
||||
return matches;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user