feat: add import/export for JSON, CSV, and Trello formats
This commit is contained in:
@@ -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 */}
|
||||
|
||||
125
src/components/import-export/ImportExportButtons.tsx
Normal file
125
src/components/import-export/ImportExportButtons.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user