feat: add import/export for JSON, CSV, and Trello formats

This commit is contained in:
Your Name
2026-02-15 19:14:06 +02:00
parent 491c089be6
commit e020ba6e8b
3 changed files with 370 additions and 8 deletions

View File

@@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button";
import { useAppStore } from "@/stores/app-store";
import { BoardCard } from "@/components/boards/BoardCard";
import { NewBoardDialog } from "@/components/boards/NewBoardDialog";
import { ImportExportButtons } from "@/components/import-export/ImportExportButtons";
export function BoardList() {
const boards = useAppStore((s) => s.boards);
@@ -27,10 +28,13 @@ export function BoardList() {
<p className="font-mono text-sm text-pylon-text-secondary">
Create your first board
</p>
<Button onClick={() => setDialogOpen(true)}>
<Plus className="size-4" />
New Board
</Button>
<div className="flex items-center gap-2">
<Button onClick={() => setDialogOpen(true)}>
<Plus className="size-4" />
New Board
</Button>
<ImportExportButtons />
</div>
</div>
<NewBoardDialog open={dialogOpen} onOpenChange={setDialogOpen} />
</>
@@ -45,10 +49,13 @@ export function BoardList() {
<h2 className="font-mono text-xs font-semibold uppercase tracking-widest text-pylon-text-secondary">
Your Boards
</h2>
<Button size="sm" onClick={() => setDialogOpen(true)}>
<Plus className="size-4" />
New
</Button>
<div className="flex items-center gap-2">
<ImportExportButtons />
<Button size="sm" onClick={() => setDialogOpen(true)}>
<Plus className="size-4" />
New
</Button>
</div>
</div>
{/* Board grid */}

View File

@@ -0,0 +1,125 @@
import { useRef } from "react";
import { Download, Upload } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useAppStore } from "@/stores/app-store";
import { useBoardStore } from "@/stores/board-store";
import { saveBoard } from "@/lib/storage";
import {
exportBoardAsJson,
exportBoardAsCsv,
importBoardFromJson,
importFromTrelloJson,
} from "@/lib/import-export";
function downloadBlob(content: string, filename: string, mimeType: string) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
export function ImportExportButtons() {
const fileInputRef = useRef<HTMLInputElement>(null);
const board = useBoardStore((s) => s.board);
const refreshBoards = useAppStore((s) => s.refreshBoards);
const setView = useAppStore((s) => s.setView);
const addRecentBoard = useAppStore((s) => s.addRecentBoard);
const openBoard = useBoardStore((s) => s.openBoard);
function handleExportJson() {
if (!board) return;
const json = exportBoardAsJson(board);
const safeName = board.title.replace(/[^a-zA-Z0-9-_]/g, "_");
downloadBlob(json, `${safeName}.json`, "application/json");
}
function handleExportCsv() {
if (!board) return;
const csv = exportBoardAsCsv(board);
const safeName = board.title.replace(/[^a-zA-Z0-9-_]/g, "_");
downloadBlob(csv, `${safeName}.csv`, "text/csv");
}
function handleImportClick() {
fileInputRef.current?.click();
}
async function handleFileSelected(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
try {
const text = await file.text();
let imported;
// Try to detect if it's a Trello export
const parsed = JSON.parse(text);
if (parsed.lists && parsed.cards && !parsed.columns) {
// Looks like Trello format
imported = importFromTrelloJson(text);
} else {
imported = importBoardFromJson(text);
}
await saveBoard(imported);
await refreshBoards();
await openBoard(imported.id);
setView({ type: "board", boardId: imported.id });
addRecentBoard(imported.id);
} catch (err) {
console.error("Import failed:", err);
// Could show a toast here in the future
}
// Reset the input so the same file can be re-imported
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}
return (
<div className="flex gap-2">
{/* Import button */}
<Button variant="outline" size="sm" onClick={handleImportClick}>
<Upload className="size-4" />
Import
</Button>
<input
ref={fileInputRef}
type="file"
accept=".json"
onChange={handleFileSelected}
className="hidden"
/>
{/* Export dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" disabled={!board}>
<Download className="size-4" />
Export
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleExportJson}>
Export as JSON
</DropdownMenuItem>
<DropdownMenuItem onClick={handleExportCsv}>
Export as CSV
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}