feat: Add note viewing dialog and implement grid/list view toggle for notes.
This commit is contained in:
@@ -12,6 +12,7 @@ import ReactMarkdown from "react-markdown";
|
|||||||
import { getNoteColor } from "@/lib/constants";
|
import { getNoteColor } from "@/lib/constants";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { VersionHistoryDialog } from "./version-history-dialog";
|
import { VersionHistoryDialog } from "./version-history-dialog";
|
||||||
|
import { NoteViewDialog } from "./note-view-dialog";
|
||||||
|
|
||||||
interface NoteCardProps {
|
interface NoteCardProps {
|
||||||
note: Note;
|
note: Note;
|
||||||
@@ -22,9 +23,11 @@ export function NoteCard({ note }: NoteCardProps) {
|
|||||||
const { mutate: updateNote } = useUpdateNote();
|
const { mutate: updateNote } = useUpdateNote();
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [historyOpen, setHistoryOpen] = useState(false);
|
const [historyOpen, setHistoryOpen] = useState(false);
|
||||||
|
const [viewOpen, setViewOpen] = useState(false);
|
||||||
|
|
||||||
// Archive toggle
|
// Archive toggle
|
||||||
const toggleArchive = () => {
|
const toggleArchive = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
updateNote({
|
updateNote({
|
||||||
id: note.id,
|
id: note.id,
|
||||||
is_archived: !note.is_archived
|
is_archived: !note.is_archived
|
||||||
@@ -32,14 +35,16 @@ export function NoteCard({ note }: NoteCardProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Pin toggle
|
// Pin toggle
|
||||||
const togglePin = () => {
|
const togglePin = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
updateNote({
|
updateNote({
|
||||||
id: note.id,
|
id: note.id,
|
||||||
is_pinned: !note.is_pinned
|
is_pinned: !note.is_pinned
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
if (confirm("Are you sure?")) {
|
if (confirm("Are you sure?")) {
|
||||||
deleteNote(note.id);
|
deleteNote(note.id);
|
||||||
}
|
}
|
||||||
@@ -66,11 +71,14 @@ export function NoteCard({ note }: NoteCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className={clsx(
|
<Card
|
||||||
"relative group transition-all hover:shadow-md",
|
className={clsx(
|
||||||
|
"relative group transition-all hover:shadow-md cursor-pointer",
|
||||||
colorClass,
|
colorClass,
|
||||||
note.is_pinned ? 'border-primary shadow-sm' : ''
|
note.is_pinned ? 'border-primary shadow-sm' : ''
|
||||||
)}>
|
)}
|
||||||
|
onClick={() => setViewOpen(true)}
|
||||||
|
>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<CardTitle className="text-lg font-semibold line-clamp-1">{note.title}</CardTitle>
|
<CardTitle className="text-lg font-semibold line-clamp-1">{note.title}</CardTitle>
|
||||||
@@ -94,10 +102,10 @@ export function NoteCard({ note }: NoteCardProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end w-full gap-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">
|
<div className="flex justify-end w-full gap-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-black/5 dark:hover:bg-white/10" onClick={() => setHistoryOpen(true)} title="History">
|
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-black/5 dark:hover:bg-white/10" onClick={(e) => { e.stopPropagation(); setHistoryOpen(true); }} title="History">
|
||||||
<History className="h-4 w-4" />
|
<History className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-black/5 dark:hover:bg-white/10" onClick={() => setEditing(true)}>
|
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-black/5 dark:hover:bg-white/10" onClick={(e) => { e.stopPropagation(); setEditing(true); }}>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-black/5 dark:hover:bg-white/10" onClick={togglePin}>
|
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-black/5 dark:hover:bg-white/10" onClick={togglePin}>
|
||||||
@@ -138,6 +146,14 @@ export function NoteCard({ note }: NoteCardProps) {
|
|||||||
noteId={note.id}
|
noteId={note.id}
|
||||||
noteTitle={note.title}
|
noteTitle={note.title}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<NoteViewDialog
|
||||||
|
open={viewOpen}
|
||||||
|
onOpenChange={setViewOpen}
|
||||||
|
note={note}
|
||||||
|
onEdit={() => setEditing(true)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
65
k-notes-frontend/src/components/note-view-dialog.tsx
Normal file
65
k-notes-frontend/src/components/note-view-dialog.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { type Note } from "@/hooks/use-notes";
|
||||||
|
import { Edit, Calendar, Pin } from "lucide-react";
|
||||||
|
import { getNoteColor } from "@/lib/constants";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
|
||||||
|
interface NoteViewDialogProps {
|
||||||
|
note: Note;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onEdit: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NoteViewDialog({ note, open, onOpenChange, onEdit }: NoteViewDialogProps) {
|
||||||
|
const colorClass = getNoteColor(note.color);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className={clsx("max-w-3xl max-h-[85vh] flex flex-col p-6 gap-0 overflow-hidden", colorClass)}>
|
||||||
|
<DialogHeader className="pb-4 shrink-0">
|
||||||
|
<div className="flex justify-between items-start gap-4">
|
||||||
|
<DialogTitle className="text-2xl font-bold leading-tight break-words">
|
||||||
|
{note.title}
|
||||||
|
</DialogTitle>
|
||||||
|
{note.is_pinned && (
|
||||||
|
<Pin className="h-5 w-5 text-primary rotate-45 shrink-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-sm text-muted-foreground gap-2 mt-1">
|
||||||
|
<Calendar className="h-3.5 w-3.5" />
|
||||||
|
<span>Created {format(new Date(note.created_at), "MMMM d, yyyy 'at' h:mm a")}</span>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex-1 min-h-0 overflow-y-auto -mx-6 px-6">
|
||||||
|
<div className="prose dark:prose-invert max-w-none text-base leading-relaxed break-words pb-6">
|
||||||
|
<ReactMarkdown>{note.content}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="pt-4 mt-2 border-t border-black/5 dark:border-white/5 flex sm:justify-between items-center gap-4 shrink-0">
|
||||||
|
<div className="flex flex-wrap gap-1.5 flex-1">
|
||||||
|
{note.tags.map(tag => (
|
||||||
|
<Badge key={tag.id} variant="secondary" className="bg-black/5 hover:bg-black/10 dark:bg-white/10 dark:hover:bg-white/20">
|
||||||
|
{tag.name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => {
|
||||||
|
onOpenChange(false);
|
||||||
|
onEdit();
|
||||||
|
}}>
|
||||||
|
<Edit className="h-4 w-4 mr-2" />
|
||||||
|
Edit Note
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,13 +3,18 @@ import { useNotes, useSearchNotes } from "@/hooks/use-notes";
|
|||||||
import { CreateNoteDialog } from "@/components/create-note-dialog";
|
import { CreateNoteDialog } from "@/components/create-note-dialog";
|
||||||
import { NoteCard } from "@/components/note-card";
|
import { NoteCard } from "@/components/note-card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Search } from "lucide-react";
|
import { Search, LayoutGrid, List } from "lucide-react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isArchive = location.pathname === "/archive";
|
const isArchive = location.pathname === "/archive";
|
||||||
|
|
||||||
|
// View mode state
|
||||||
|
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
@@ -36,7 +41,29 @@ export default function DashboardPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isArchive && <CreateNoteDialog />}
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex items-center bg-muted/50 p-1 rounded-lg border">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={clsx("h-8 w-8", viewMode === "grid" && "bg-background shadow-sm")}
|
||||||
|
onClick={() => setViewMode("grid")}
|
||||||
|
title="Grid View"
|
||||||
|
>
|
||||||
|
<LayoutGrid className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={clsx("h-8 w-8", viewMode === "list" && "bg-background shadow-sm")}
|
||||||
|
onClick={() => setViewMode("list")}
|
||||||
|
title="List View"
|
||||||
|
>
|
||||||
|
<List className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{!isArchive && <CreateNoteDialog />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
@@ -65,8 +92,12 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Notes Grid */}
|
{/* Notes Grid/List */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 items-start">
|
<div className={clsx(
|
||||||
|
viewMode === "grid"
|
||||||
|
? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 items-start"
|
||||||
|
: "flex flex-col gap-4 max-w-3xl mx-auto"
|
||||||
|
)}>
|
||||||
{/* Pinned Notes First (if not searching and not archive) */}
|
{/* Pinned Notes First (if not searching and not archive) */}
|
||||||
{!searchQuery && !isArchive && displayNotes?.filter((n: any) => n.is_pinned).map((note: any) => (
|
{!searchQuery && !isArchive && displayNotes?.filter((n: any) => n.is_pinned).map((note: any) => (
|
||||||
<NoteCard key={note.id} note={note} />
|
<NoteCard key={note.id} note={note} />
|
||||||
|
|||||||
Reference in New Issue
Block a user