"use client";
import { useState, useEffect } from "react";
import { read, utils, type WorkBook } from "xlsx";
const SPREADSHEET_EXTENSIONS = new Set([
"xlsx", "xls", "xlsb", "xlsm", "xltx", "xltm",
"ods", "fods",
"csv", "tsv",
"numbers",
]);
export function isSpreadsheetFile(filename: string): boolean {
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
return SPREADSHEET_EXTENSIONS.has(ext);
}
type FileViewerProps =
| { content: string; filename: string; type: "yaml" | "text" }
| { filename: string; type: "spreadsheet"; url: string; content?: never };
export function FileViewer(props: FileViewerProps) {
if (props.type === "spreadsheet") {
return ;
}
const { content, filename, type } = props;
const lines = content.split("\n");
return (
{lines.map((line, idx) => (
{idx + 1}
{type === "yaml" ? (
) : (
line || " "
)}
))}
);
}
function FileHeader({ filename, label, icon }: { filename: string; label: string; icon?: React.ReactNode }) {
return (
{icon ?? (
)}
{filename}
{label}
);
}
// ---------------------------------------------------------------------------
// Spreadsheet viewer
// ---------------------------------------------------------------------------
function SpreadsheetViewer({ filename, url }: { filename: string; url: string }) {
const [workbook, setWorkbook] = useState(null);
const [activeSheet, setActiveSheet] = useState(0);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setWorkbook(null);
setActiveSheet(0);
setError(null);
fetch(url)
.then((res) => {
if (!res.ok) {throw new Error(`Failed to load file (${res.status})`);}
return res.arrayBuffer();
})
.then((buf) => {
if (cancelled) {return;}
const wb = read(buf, { type: "array" });
setWorkbook(wb);
})
.catch((err) => {
if (!cancelled) {setError(String(err));}
});
return () => { cancelled = true; };
}, [url]);
const ext = filename.split(".").pop()?.toUpperCase() ?? "SPREADSHEET";
if (error) {
return (
} />
Failed to load spreadsheet: {error}
);
}
if (!workbook) {
return (
} />
Loading spreadsheet...
);
}
const sheetNames = workbook.SheetNames;
const sheet = workbook.Sheets[sheetNames[activeSheet]];
const rows: string[][] = sheet ? utils.sheet_to_json(sheet, { header: 1, defval: "" }) : [];
return (
} />
{/* Sheet tabs */}
{sheetNames.length > 1 && (
{sheetNames.map((name, idx) => (
))}
)}
{/* Table */}
{rows.length === 0 ? (
This sheet is empty.
) : (
{/* Row number header */}
|
{rows[0]?.map((_cell, colIdx) => (
{columnLabel(colIdx)}
|
))}
{rows.map((row, rowIdx) => (
|
{rowIdx + 1}
|
{row.map((cell, colIdx) => (
{String(cell)}
|
))}
))}
)}
{rows.length} row{rows.length !== 1 ? "s" : ""}
{rows[0] ? ` \u00d7 ${rows[0].length} column${rows[0].length !== 1 ? "s" : ""}` : ""}
{sheetNames.length > 1 ? ` \u00b7 ${sheetNames.length} sheets` : ""}
);
}
/** Convert zero-based column index to Excel-style label (A, B, ..., Z, AA, AB, ...) */
function columnLabel(idx: number): string {
let label = "";
let n = idx;
do {
label = String.fromCharCode(65 + (n % 26)) + label;
n = Math.floor(n / 26) - 1;
} while (n >= 0);
return label;
}
function SpreadsheetIcon() {
return (
);
}
/** Simple YAML syntax highlighting */
function YamlLine({ line }: { line: string }) {
// Comment
if (line.trim().startsWith("#")) {
return {line};
}
// Key: value
const kvMatch = line.match(/^(\s*)([\w][\w_-]*)\s*(:)(.*)/);
if (kvMatch) {
const [, indent, key, colon, value] = kvMatch;
return (
<>
{indent}
{key}
{colon}
>
);
}
// List item
const listMatch = line.match(/^(\s*)(-)(\s*)(.*)/);
if (listMatch) {
const [, indent, dash, space, value] = listMatch;
return (
<>
{indent}
{dash}
{space}
{value}
>
);
}
return {line || " "};
}
function YamlValue({ value }: { value: string }) {
const trimmed = value.trim();
// String in quotes
if (
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
(trimmed.startsWith("'") && trimmed.endsWith("'"))
) {
return {trimmed};
}
// Boolean
if (trimmed === "true" || trimmed === "false") {
return {trimmed};
}
// Number
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
return {trimmed};
}
// Null
if (trimmed === "null") {
return (
{" "}
{trimmed}
);
}
return {value};
}