wiki-viewer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.github/workflows/publish.yml +37 -0
- package/.next/standalone/DESIGN.md +521 -0
- package/.next/standalone/README.md +283 -0
- package/.next/standalone/bin/wiki-viewer.js +158 -0
- package/.next/standalone/next.config.ts +11 -0
- package/.next/standalone/package.json +120 -0
- package/.next/standalone/pnpm-lock.yaml +5654 -0
- package/.next/standalone/pnpm-workspace.yaml +2 -0
- package/.next/standalone/postcss.config.mjs +2 -0
- package/.next/standalone/public/logo.svg +11 -0
- package/.next/standalone/public/public/logo.svg +11 -0
- package/.next/standalone/server.js +44 -0
- package/.next/standalone/src/app/api/app-proxy/[...path]/route.ts +247 -0
- package/.next/standalone/src/app/api/assets/[...path]/route.ts +70 -0
- package/.next/standalone/src/app/api/system/browse/route.ts +78 -0
- package/.next/standalone/src/app/api/system/clear-root/route.ts +9 -0
- package/.next/standalone/src/app/api/system/config/route.ts +10 -0
- package/.next/standalone/src/app/api/system/pins/route.ts +31 -0
- package/.next/standalone/src/app/api/system/reveal/route.ts +28 -0
- package/.next/standalone/src/app/api/system/root-status/route.ts +9 -0
- package/.next/standalone/src/app/api/system/set-root/route.ts +23 -0
- package/.next/standalone/src/app/api/upload/[...path]/route.ts +80 -0
- package/.next/standalone/src/app/api/wiki/app/route.ts +41 -0
- package/.next/standalone/src/app/api/wiki/content/route.ts +60 -0
- package/.next/standalone/src/app/api/wiki/file/route.ts +52 -0
- package/.next/standalone/src/app/api/wiki/folder/route.ts +26 -0
- package/.next/standalone/src/app/api/wiki/move/route.ts +45 -0
- package/.next/standalone/src/app/api/wiki/page/route.ts +84 -0
- package/.next/standalone/src/app/api/wiki/route.ts +98 -0
- package/.next/standalone/src/app/api/wiki/slugs/route.ts +50 -0
- package/.next/standalone/src/app/api/wiki/upload/route.ts +87 -0
- package/.next/standalone/src/app/api/wiki/watch/route.ts +86 -0
- package/.next/standalone/src/app/globals.css +716 -0
- package/.next/standalone/src/app/icon.svg +11 -0
- package/.next/standalone/src/app/layout.tsx +35 -0
- package/.next/standalone/src/app/page.tsx +1509 -0
- package/.next/standalone/src/components/confirm-dialog.tsx +56 -0
- package/.next/standalone/src/components/dir-picker.tsx +393 -0
- package/.next/standalone/src/components/editor/bubble-menu.tsx +286 -0
- package/.next/standalone/src/components/editor/callout-extension.ts +36 -0
- package/.next/standalone/src/components/editor/csv-viewer.tsx +335 -0
- package/.next/standalone/src/components/editor/editor-toolbar.tsx +564 -0
- package/.next/standalone/src/components/editor/editor.tsx +633 -0
- package/.next/standalone/src/components/editor/extensions/drag-handle.ts +325 -0
- package/.next/standalone/src/components/editor/extensions/heading-anchors.ts +55 -0
- package/.next/standalone/src/components/editor/extensions/resizable-image.tsx +159 -0
- package/.next/standalone/src/components/editor/extensions.ts +148 -0
- package/.next/standalone/src/components/editor/file-fallback-viewer.tsx +74 -0
- package/.next/standalone/src/components/editor/folder-index.tsx +235 -0
- package/.next/standalone/src/components/editor/google-doc-viewer.tsx +73 -0
- package/.next/standalone/src/components/editor/image-viewer.tsx +56 -0
- package/.next/standalone/src/components/editor/link-popover.tsx +111 -0
- package/.next/standalone/src/components/editor/media-popover.tsx +211 -0
- package/.next/standalone/src/components/editor/media-viewer.tsx +69 -0
- package/.next/standalone/src/components/editor/mermaid-viewer.tsx +285 -0
- package/.next/standalone/src/components/editor/node-app-viewer.tsx +269 -0
- package/.next/standalone/src/components/editor/notebook-viewer.tsx +370 -0
- package/.next/standalone/src/components/editor/office/docx-viewer.tsx +92 -0
- package/.next/standalone/src/components/editor/office/office-chrome.tsx +82 -0
- package/.next/standalone/src/components/editor/office/pptx-viewer.tsx +103 -0
- package/.next/standalone/src/components/editor/office/xlsx-viewer.tsx +116 -0
- package/.next/standalone/src/components/editor/pdf-viewer.tsx +31 -0
- package/.next/standalone/src/components/editor/slash-commands.tsx +454 -0
- package/.next/standalone/src/components/editor/source-viewer.tsx +212 -0
- package/.next/standalone/src/components/editor/table-menu.tsx +232 -0
- package/.next/standalone/src/components/editor/website-viewer.tsx +74 -0
- package/.next/standalone/src/components/editor/wiki-link-create-dialog.tsx +188 -0
- package/.next/standalone/src/components/editor/wiki-link-decorator.ts +79 -0
- package/.next/standalone/src/components/editor/wiki-link-extension.ts +120 -0
- package/.next/standalone/src/components/editor/wiki-link-picker.tsx +250 -0
- package/.next/standalone/src/components/layout/viewer-toolbar.tsx +54 -0
- package/.next/standalone/src/components/theme-provider.tsx +16 -0
- package/.next/standalone/src/components/theme-toggle.tsx +24 -0
- package/.next/standalone/src/components/ui/badge.tsx +35 -0
- package/.next/standalone/src/components/ui/button.tsx +56 -0
- package/.next/standalone/src/components/ui/card.tsx +63 -0
- package/.next/standalone/src/components/ui/collapsible.tsx +9 -0
- package/.next/standalone/src/components/ui/command.tsx +150 -0
- package/.next/standalone/src/components/ui/context-menu.tsx +191 -0
- package/.next/standalone/src/components/ui/dialog.tsx +119 -0
- package/.next/standalone/src/components/ui/dropdown-menu.tsx +194 -0
- package/.next/standalone/src/components/ui/input.tsx +22 -0
- package/.next/standalone/src/components/ui/label.tsx +25 -0
- package/.next/standalone/src/components/ui/popover.tsx +33 -0
- package/.next/standalone/src/components/ui/scroll-area.tsx +47 -0
- package/.next/standalone/src/components/ui/select.tsx +156 -0
- package/.next/standalone/src/components/ui/separator.tsx +30 -0
- package/.next/standalone/src/components/ui/sheet.tsx +140 -0
- package/.next/standalone/src/components/ui/skeleton.tsx +15 -0
- package/.next/standalone/src/components/ui/switch.tsx +28 -0
- package/.next/standalone/src/components/ui/tabs.tsx +54 -0
- package/.next/standalone/src/components/ui/textarea.tsx +21 -0
- package/.next/standalone/src/components/ui/tip.tsx +23 -0
- package/.next/standalone/src/components/ui/toggle-group.tsx +41 -0
- package/.next/standalone/src/components/ui/tooltip.tsx +27 -0
- package/.next/standalone/src/components/wiki/frontmatter-header.tsx +164 -0
- package/.next/standalone/src/lib/app-runner.ts +231 -0
- package/.next/standalone/src/lib/cabinets/tree.ts +75 -0
- package/.next/standalone/src/lib/config.ts +32 -0
- package/.next/standalone/src/lib/embeds/detect.ts +142 -0
- package/.next/standalone/src/lib/google/detect.ts +107 -0
- package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +84 -0
- package/.next/standalone/src/lib/markdown/remark-wikilinks.ts +100 -0
- package/.next/standalone/src/lib/markdown/to-html.ts +168 -0
- package/.next/standalone/src/lib/markdown/to-markdown.ts +238 -0
- package/.next/standalone/src/lib/root-dir.ts +35 -0
- package/.next/standalone/src/lib/toast.ts +9 -0
- package/.next/standalone/src/lib/utils.ts +6 -0
- package/.next/standalone/src/lib/wiki-helpers.ts +36 -0
- package/.next/standalone/src/stores/ai-panel-store.ts +23 -0
- package/.next/standalone/src/stores/editor-store.ts +244 -0
- package/.next/standalone/src/stores/tree-store.ts +52 -0
- package/.next/standalone/src/stores/wiki-slugs-store.ts +74 -0
- package/.next/standalone/src/types/index.ts +47 -0
- package/.next/standalone/tailwind.config.ts +201 -0
- package/.next/standalone/tsconfig.json +41 -0
- package/README.md +283 -0
- package/bin/wiki-viewer.js +158 -0
- package/package.json +120 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Loader2 } from "lucide-react";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { OfficeChrome } from "./office-chrome";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
path: string;
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function DocxViewer({ path, title }: Props) {
|
|
13
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
let cancelled = false;
|
|
19
|
+
const container = containerRef.current;
|
|
20
|
+
if (!container) return;
|
|
21
|
+
|
|
22
|
+
// Clear any previous render
|
|
23
|
+
container.innerHTML = "";
|
|
24
|
+
|
|
25
|
+
(async () => {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
try {
|
|
29
|
+
const [{ renderAsync }, res] = await Promise.all([
|
|
30
|
+
import("docx-preview"),
|
|
31
|
+
fetch(`/api/assets/${path}`),
|
|
32
|
+
]);
|
|
33
|
+
if (cancelled) return;
|
|
34
|
+
if (!res.ok) throw new Error(`Failed to load file (${res.status})`);
|
|
35
|
+
const blob = await res.blob();
|
|
36
|
+
if (cancelled) return;
|
|
37
|
+
await renderAsync(blob, container, undefined, {
|
|
38
|
+
className: "docx-rendered",
|
|
39
|
+
inWrapper: true,
|
|
40
|
+
ignoreWidth: false,
|
|
41
|
+
ignoreHeight: false,
|
|
42
|
+
breakPages: true,
|
|
43
|
+
renderHeaders: true,
|
|
44
|
+
renderFooters: true,
|
|
45
|
+
renderFootnotes: true,
|
|
46
|
+
experimental: true,
|
|
47
|
+
useBase64URL: true,
|
|
48
|
+
});
|
|
49
|
+
if (!cancelled) setLoading(false);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (!cancelled) {
|
|
52
|
+
setError(
|
|
53
|
+
err instanceof Error ? err.message : "Failed to render document",
|
|
54
|
+
);
|
|
55
|
+
setLoading(false);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
cancelled = true;
|
|
62
|
+
};
|
|
63
|
+
}, [path]);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
67
|
+
<OfficeChrome path={path} title={title} extLabel="DOCX" />
|
|
68
|
+
<div className="flex-1 overflow-y-auto bg-muted/30">
|
|
69
|
+
{loading && !error && (
|
|
70
|
+
<div className="h-full flex items-center justify-center text-muted-foreground">
|
|
71
|
+
<Loader2 className="h-5 w-5 animate-spin mr-2" />
|
|
72
|
+
Rendering document…
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
{error && (
|
|
76
|
+
<div className="h-full flex items-center justify-center">
|
|
77
|
+
<div className="text-center space-y-2">
|
|
78
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
79
|
+
<p className="text-xs text-muted-foreground">
|
|
80
|
+
Try downloading the file and opening it externally.
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
<div
|
|
86
|
+
ref={containerRef}
|
|
87
|
+
className="docx-viewer-body mx-auto max-w-5xl py-6 px-4"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Download, ExternalLink, FolderOpen } from "lucide-react";
|
|
4
|
+
import { ViewerToolbar } from "@/components/layout/viewer-toolbar";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
|
|
7
|
+
interface OfficeChromeProps {
|
|
8
|
+
path: string;
|
|
9
|
+
title: string;
|
|
10
|
+
extLabel: string;
|
|
11
|
+
/** Optional external "open in source" action (e.g. Open in Google). */
|
|
12
|
+
external?: { label: string; href: string };
|
|
13
|
+
/** Hide the "Open in Finder" button (useful for Google embeds that aren't on disk). */
|
|
14
|
+
hideFinder?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function OfficeChrome({
|
|
18
|
+
path,
|
|
19
|
+
extLabel,
|
|
20
|
+
external,
|
|
21
|
+
hideFinder,
|
|
22
|
+
}: OfficeChromeProps) {
|
|
23
|
+
const assetUrl = `/api/assets/${path}`;
|
|
24
|
+
const filename = path.split("/").pop() || path;
|
|
25
|
+
|
|
26
|
+
const revealInFinder = async () => {
|
|
27
|
+
try {
|
|
28
|
+
await fetch("/api/system/reveal", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
body: JSON.stringify({ path }),
|
|
32
|
+
});
|
|
33
|
+
} catch {
|
|
34
|
+
/* ignore */
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ViewerToolbar path={path} badge={extLabel || undefined}>
|
|
40
|
+
{external && (
|
|
41
|
+
<a
|
|
42
|
+
href={external.href}
|
|
43
|
+
target="_blank"
|
|
44
|
+
rel="noreferrer noopener"
|
|
45
|
+
className="inline-flex items-center gap-1.5 text-[11px] h-7 px-2.5 rounded-md border border-border hover:bg-accent transition-colors"
|
|
46
|
+
>
|
|
47
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
48
|
+
{external.label}
|
|
49
|
+
</a>
|
|
50
|
+
)}
|
|
51
|
+
{!hideFinder && (
|
|
52
|
+
<Button
|
|
53
|
+
variant="outline"
|
|
54
|
+
size="sm"
|
|
55
|
+
className="gap-1.5 text-[11px] h-7"
|
|
56
|
+
onClick={revealInFinder}
|
|
57
|
+
title="Open in Finder"
|
|
58
|
+
>
|
|
59
|
+
<FolderOpen className="h-3.5 w-3.5" />
|
|
60
|
+
Reveal
|
|
61
|
+
</Button>
|
|
62
|
+
)}
|
|
63
|
+
{!hideFinder && (
|
|
64
|
+
<Button
|
|
65
|
+
variant="outline"
|
|
66
|
+
size="sm"
|
|
67
|
+
className="gap-1.5 text-[11px] h-7"
|
|
68
|
+
onClick={() => {
|
|
69
|
+
const a = document.createElement("a");
|
|
70
|
+
a.href = assetUrl;
|
|
71
|
+
a.download = filename;
|
|
72
|
+
a.click();
|
|
73
|
+
}}
|
|
74
|
+
title="Download original"
|
|
75
|
+
>
|
|
76
|
+
<Download className="h-3.5 w-3.5" />
|
|
77
|
+
Download
|
|
78
|
+
</Button>
|
|
79
|
+
)}
|
|
80
|
+
</ViewerToolbar>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Loader2 } from "lucide-react";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { OfficeChrome } from "./office-chrome";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
path: string;
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function PptxViewer({ path, title }: Props) {
|
|
13
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
let cancelled = false;
|
|
19
|
+
const container = containerRef.current;
|
|
20
|
+
if (!container) return;
|
|
21
|
+
container.innerHTML = "";
|
|
22
|
+
|
|
23
|
+
let previewer: { destroy?: () => void } | null = null;
|
|
24
|
+
|
|
25
|
+
(async () => {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
try {
|
|
29
|
+
const [{ init }, res] = await Promise.all([
|
|
30
|
+
import("pptx-preview"),
|
|
31
|
+
fetch(`/api/assets/${path}`),
|
|
32
|
+
]);
|
|
33
|
+
if (cancelled) return;
|
|
34
|
+
if (!res.ok) throw new Error(`Failed to load file (${res.status})`);
|
|
35
|
+
const buf = await res.arrayBuffer();
|
|
36
|
+
if (cancelled) return;
|
|
37
|
+
|
|
38
|
+
const width = container.clientWidth || 960;
|
|
39
|
+
const height = Math.round((width * 9) / 16);
|
|
40
|
+
previewer = init(container, {
|
|
41
|
+
width,
|
|
42
|
+
height,
|
|
43
|
+
mode: "list",
|
|
44
|
+
}) as unknown as {
|
|
45
|
+
destroy?: () => void;
|
|
46
|
+
preview: (buf: ArrayBuffer) => Promise<unknown>;
|
|
47
|
+
};
|
|
48
|
+
await (
|
|
49
|
+
previewer as unknown as {
|
|
50
|
+
preview: (b: ArrayBuffer) => Promise<unknown>;
|
|
51
|
+
}
|
|
52
|
+
).preview(buf);
|
|
53
|
+
if (!cancelled) setLoading(false);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (!cancelled) {
|
|
56
|
+
setError(
|
|
57
|
+
err instanceof Error
|
|
58
|
+
? err.message
|
|
59
|
+
: "Failed to render presentation",
|
|
60
|
+
);
|
|
61
|
+
setLoading(false);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
|
|
66
|
+
return () => {
|
|
67
|
+
cancelled = true;
|
|
68
|
+
try {
|
|
69
|
+
previewer?.destroy?.();
|
|
70
|
+
} catch {
|
|
71
|
+
/* ignore */
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}, [path]);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
78
|
+
<OfficeChrome path={path} title={title} extLabel="PPTX" />
|
|
79
|
+
<div className="flex-1 overflow-auto bg-muted/30 py-4">
|
|
80
|
+
{loading && !error && (
|
|
81
|
+
<div className="h-[60vh] flex items-center justify-center text-muted-foreground">
|
|
82
|
+
<Loader2 className="h-5 w-5 animate-spin mr-2" />
|
|
83
|
+
Rendering slides…
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
{error && (
|
|
87
|
+
<div className="h-[60vh] flex items-center justify-center">
|
|
88
|
+
<div className="text-center space-y-2">
|
|
89
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
90
|
+
<p className="text-xs text-muted-foreground">
|
|
91
|
+
Try downloading the file and opening it externally.
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
<div
|
|
97
|
+
ref={containerRef}
|
|
98
|
+
className="pptx-viewer-body mx-auto max-w-5xl px-4"
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Loader2 } from "lucide-react";
|
|
4
|
+
import { useEffect, useMemo, useState } from "react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { OfficeChrome } from "./office-chrome";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
path: string;
|
|
10
|
+
title: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Sheet {
|
|
14
|
+
name: string;
|
|
15
|
+
html: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function XlsxViewer({ path, title }: Props) {
|
|
19
|
+
const [sheets, setSheets] = useState<Sheet[] | null>(null);
|
|
20
|
+
const [active, setActive] = useState(0);
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
const [error, setError] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
let cancelled = false;
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
setSheets(null);
|
|
29
|
+
(async () => {
|
|
30
|
+
try {
|
|
31
|
+
const [XLSX, res] = await Promise.all([
|
|
32
|
+
import("xlsx"),
|
|
33
|
+
fetch(`/api/assets/${path}`),
|
|
34
|
+
]);
|
|
35
|
+
if (cancelled) return;
|
|
36
|
+
if (!res.ok) throw new Error(`Failed to load file (${res.status})`);
|
|
37
|
+
const buf = await res.arrayBuffer();
|
|
38
|
+
if (cancelled) return;
|
|
39
|
+
const wb = XLSX.read(buf, {
|
|
40
|
+
type: "array",
|
|
41
|
+
cellDates: true,
|
|
42
|
+
cellStyles: true,
|
|
43
|
+
});
|
|
44
|
+
const result: Sheet[] = wb.SheetNames.map((name) => ({
|
|
45
|
+
name,
|
|
46
|
+
html: XLSX.utils.sheet_to_html(wb.Sheets[name], { editable: false }),
|
|
47
|
+
}));
|
|
48
|
+
if (cancelled) return;
|
|
49
|
+
setSheets(result);
|
|
50
|
+
setActive(0);
|
|
51
|
+
setLoading(false);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (!cancelled) {
|
|
54
|
+
setError(
|
|
55
|
+
err instanceof Error ? err.message : "Failed to parse spreadsheet",
|
|
56
|
+
);
|
|
57
|
+
setLoading(false);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})();
|
|
61
|
+
return () => {
|
|
62
|
+
cancelled = true;
|
|
63
|
+
};
|
|
64
|
+
}, [path]);
|
|
65
|
+
|
|
66
|
+
const current = useMemo(() => sheets?.[active] ?? null, [sheets, active]);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
70
|
+
<OfficeChrome path={path} title={title} extLabel="XLSX" />
|
|
71
|
+
{sheets && sheets.length > 1 && (
|
|
72
|
+
<div className="flex items-center gap-0.5 border-b border-border bg-muted/40 px-2 overflow-x-auto scrollbar-none">
|
|
73
|
+
{sheets.map((s, i) => (
|
|
74
|
+
<button
|
|
75
|
+
key={s.name + i}
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={() => setActive(i)}
|
|
78
|
+
className={cn(
|
|
79
|
+
"px-3 py-1.5 text-[12px] rounded-t whitespace-nowrap transition-colors",
|
|
80
|
+
i === active
|
|
81
|
+
? "bg-background text-foreground font-medium border-t border-x border-border -mb-px"
|
|
82
|
+
: "text-muted-foreground hover:text-foreground",
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
{s.name}
|
|
86
|
+
</button>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
<div className="flex-1 overflow-auto">
|
|
91
|
+
{loading && !error && (
|
|
92
|
+
<div className="h-full flex items-center justify-center text-muted-foreground">
|
|
93
|
+
<Loader2 className="h-5 w-5 animate-spin mr-2" />
|
|
94
|
+
Parsing spreadsheet…
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
{error && (
|
|
98
|
+
<div className="h-full flex items-center justify-center">
|
|
99
|
+
<div className="text-center space-y-2">
|
|
100
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
101
|
+
<p className="text-xs text-muted-foreground">
|
|
102
|
+
Try downloading the file and opening it externally.
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
{current && (
|
|
108
|
+
<div
|
|
109
|
+
className="xlsx-sheet p-3 text-[12px]"
|
|
110
|
+
dangerouslySetInnerHTML={{ __html: current.html }}
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ExternalLink } from "lucide-react";
|
|
4
|
+
import { ViewerToolbar } from "@/components/layout/viewer-toolbar";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
|
|
7
|
+
interface PdfViewerProps {
|
|
8
|
+
path: string;
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function PdfViewer({ path, title }: PdfViewerProps) {
|
|
13
|
+
const pdfSrc = `/api/assets/${path}`;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
17
|
+
<ViewerToolbar path={path} badge="PDF">
|
|
18
|
+
<Button
|
|
19
|
+
variant="ghost"
|
|
20
|
+
size="sm"
|
|
21
|
+
className="h-7 gap-1.5 text-xs"
|
|
22
|
+
onClick={() => window.open(pdfSrc, "_blank")}
|
|
23
|
+
>
|
|
24
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
25
|
+
Open in new tab
|
|
26
|
+
</Button>
|
|
27
|
+
</ViewerToolbar>
|
|
28
|
+
<iframe src={pdfSrc} className="flex-1 w-full border-0" title={title} />
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|