notes/src/components/DatabaseView.tsx

172 lines
7.3 KiB
TypeScript
Raw Normal View History

import { useEffect, useState, useMemo } from "react";
import { useVault } from "../App";
import { queryFrontmatter, type FrontmatterRow } from "../lib/commands";
type ViewMode = "table" | "gallery" | "list";
/**
* DatabaseView Notion-style table/gallery/list views from frontmatter.
*/
export function DatabaseView() {
const { vaultPath, navigateToNote } = useVault();
const [rows, setRows] = useState<FrontmatterRow[]>([]);
const [viewMode, setViewMode] = useState<ViewMode>("table");
const [sortField, setSortField] = useState<string>("title");
const [sortAsc, setSortAsc] = useState(true);
const [filterField, setFilterField] = useState("");
const [filterValue, setFilterValue] = useState("");
useEffect(() => {
if (!vaultPath) return;
queryFrontmatter(vaultPath).then(setRows).catch(() => setRows([]));
}, [vaultPath]);
// All unique field keys
const allFields = useMemo(() => {
const keys = new Set<string>();
rows.forEach(r => Object.keys(r.fields).forEach(k => keys.add(k)));
return ["title", ...Array.from(keys)];
}, [rows]);
// Sort + Filter
const processed = useMemo(() => {
let data = [...rows];
if (filterField && filterValue) {
data = data.filter(r => {
const val = filterField === "title"
? r.title
: (r.fields[filterField] || "");
return val.toLowerCase().includes(filterValue.toLowerCase());
});
}
data.sort((a, b) => {
const va = sortField === "title" ? a.title : (a.fields[sortField] || "");
const vb = sortField === "title" ? b.title : (b.fields[sortField] || "");
return sortAsc ? va.localeCompare(vb) : vb.localeCompare(va);
});
return data;
}, [rows, sortField, sortAsc, filterField, filterValue]);
const handleSort = (field: string) => {
if (sortField === field) setSortAsc(!sortAsc);
else { setSortField(field); setSortAsc(true); }
};
return (
<div className="database-view">
<div className="database-header">
<h2 className="database-title">📊 Database</h2>
<div className="database-controls">
<div className="database-filter">
<select
className="database-select"
value={filterField}
onChange={e => setFilterField(e.target.value)}
>
<option value="">Filter by</option>
{allFields.map(f => <option key={f} value={f}>{f}</option>)}
</select>
{filterField && (
<input
className="database-filter-input"
value={filterValue}
onChange={e => setFilterValue(e.target.value)}
placeholder="Contains…"
/>
)}
</div>
<div className="database-mode-group">
{(["table", "gallery", "list"] as ViewMode[]).map(m => (
<button
key={m}
className={`database-mode-btn ${viewMode === m ? "active" : ""}`}
onClick={() => setViewMode(m)}
>
{m === "table" ? "📋" : m === "gallery" ? "🖼️" : "📝"} {m}
</button>
))}
</div>
</div>
</div>
{viewMode === "table" && (
<div className="database-table-wrapper">
<table className="database-table">
<thead>
<tr>
{allFields.map(f => (
<th key={f} onClick={() => handleSort(f)} className="database-th">
{f}
{sortField === f && <span>{sortAsc ? " ↑" : " ↓"}</span>}
</th>
))}
</tr>
</thead>
<tbody>
{processed.map(row => (
<tr
key={row.path}
className="database-row"
onClick={() => navigateToNote(row.title)}
>
{allFields.map(f => (
<td key={f} className="database-td">
{f === "title" ? row.title : (row.fields[f] || "—")}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
{viewMode === "gallery" && (
<div className="database-gallery">
{processed.map(row => (
<div
key={row.path}
className="database-gallery-card"
onClick={() => navigateToNote(row.title)}
>
<div className="gallery-card-title">{row.title}</div>
<div className="gallery-card-fields">
{Object.entries(row.fields).slice(0, 3).map(([k, v]) => (
<div key={k} className="gallery-card-field">
<span className="gallery-field-key">{k}:</span>
<span className="gallery-field-val">{v}</span>
</div>
))}
</div>
</div>
))}
</div>
)}
{viewMode === "list" && (
<div className="database-list">
{processed.map(row => (
<div
key={row.path}
className="database-list-item"
onClick={() => navigateToNote(row.title)}
>
<span className="database-list-name">{row.title}</span>
<span className="database-list-meta">
{Object.entries(row.fields).slice(0, 2).map(([k, v]) => `${k}: ${v}`).join(" • ")}
</span>
</div>
))}
</div>
)}
{processed.length === 0 && (
<div className="database-empty">
<p>No notes with frontmatter found.</p>
<p>Add YAML frontmatter between <code>---</code> markers to your notes.</p>
</div>
)}
</div>
);
}