Skip to content

Commit dc2f985

Browse files
authored
Merge pull request #140 from slaveofcode/feat/documents-docx-viewer
feat(documents): Word (DOCX) Viewer + Documents category
2 parents 5fe4668 + a99150d commit dc2f985

8 files changed

Lines changed: 238 additions & 2 deletions

File tree

package-lock.json

Lines changed: 91 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"astro": "^4.16.19",
7676
"cmdk": "^0.2.1",
7777
"comlink": "^4.4.2",
78+
"docx-preview": "^0.4.0",
7879
"dompurify": "^3.4.12",
7980
"fast-xml-parser": "^5.10.0",
8081
"fflate": "^0.8.3",

scripts/generate-og.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const fonts = [
1919
];
2020

2121
const CAT_COLOR = {
22-
Dev: '#3b82f6', PDF: '#ef4444', Image: '#22c55e', Files: '#eab308', Draw: '#a855f7',
22+
Dev: '#3b82f6', PDF: '#ef4444', Image: '#22c55e', Files: '#eab308', Documents: '#14b8a6', Draw: '#a855f7',
2323
Media: '#ec4899', Network: '#06b6d4', Maps: '#10b981', Legacy: '#6366f1', Playground: '#f97316',
2424
};
2525

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { useRef, useState } from 'react';
2+
import { FileText, Printer } from 'lucide-react';
3+
import { Dropzone } from '@/components/ui/Dropzone';
4+
import { Button } from '@/components/ui/Button';
5+
import { Alert } from '@/components/ui/Alert';
6+
import type { Lang } from '@/i18n/config';
7+
8+
const TR: Record<Lang, {
9+
intro: string; drop: string; dropSub: string; how: string;
10+
opening: string; another: string; print: string; errRead: string;
11+
}> = {
12+
en: {
13+
intro: 'Open and read a Word document (.docx) right here — full layout, tables and images. It is rendered on your device; nothing is uploaded.',
14+
drop: 'Drop a Word document (.docx)', dropSub: 'Rendered on your device — no upload.',
15+
how: 'Older .doc (binary) files aren’t supported — save as .docx in Word/Google Docs first.',
16+
opening: 'Rendering…', another: 'Open another', print: 'Print / Save as PDF',
17+
errRead: 'Could not open this document — is it a valid .docx file?',
18+
},
19+
id: {
20+
intro: 'Buka dan baca dokumen Word (.docx) langsung di sini — tata letak, tabel, dan gambar lengkap. Ditampilkan di perangkat Anda; tidak ada yang diunggah.',
21+
drop: 'Letakkan dokumen Word (.docx)', dropSub: 'Ditampilkan di perangkat Anda — tanpa unggahan.',
22+
how: 'Berkas .doc lama (biner) tidak didukung — simpan sebagai .docx di Word/Google Docs terlebih dahulu.',
23+
opening: 'Menampilkan…', another: 'Buka yang lain', print: 'Cetak / Simpan PDF',
24+
errRead: 'Tidak dapat membuka dokumen ini — apakah berkas .docx yang valid?',
25+
},
26+
};
27+
28+
export default function DocxViewer({ lang = 'en' }: { lang?: Lang }) {
29+
const t = TR[lang] ?? TR.en;
30+
const containerRef = useRef<HTMLDivElement>(null);
31+
const [hasDoc, setHasDoc] = useState(false);
32+
const [busy, setBusy] = useState(false);
33+
const [error, setError] = useState('');
34+
35+
const onDrop = async (files: File[]) => {
36+
const f = files[0];
37+
if (!f) return;
38+
setError('');
39+
setBusy(true);
40+
try {
41+
const buf = await f.arrayBuffer();
42+
const { renderAsync } = await import('docx-preview');
43+
const container = containerRef.current!;
44+
container.innerHTML = '';
45+
await renderAsync(buf, container, undefined, {
46+
className: 'docx', inWrapper: true, breakPages: true, ignoreLastRenderedPageBreak: false,
47+
});
48+
setHasDoc(true);
49+
} catch {
50+
setError(t.errRead);
51+
setHasDoc(false);
52+
} finally {
53+
setBusy(false);
54+
}
55+
};
56+
57+
const reset = () => {
58+
if (containerRef.current) containerRef.current.innerHTML = '';
59+
setHasDoc(false);
60+
setError('');
61+
};
62+
63+
return (
64+
<div className="space-y-4">
65+
<p className="text-sm text-muted-foreground print:hidden">{t.intro}</p>
66+
67+
{!hasDoc && (
68+
<div className="print:hidden">
69+
<Dropzone onDrop={onDrop} accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document" multiple={false}>
70+
<div className="space-y-1">
71+
<p className="flex items-center justify-center gap-2 text-lg font-bold"><FileText className="h-5 w-5" /> {busy ? t.opening : t.drop}</p>
72+
<p className="text-sm text-muted-foreground">{t.dropSub}</p>
73+
</div>
74+
</Dropzone>
75+
<p className="mt-2 text-xs text-muted-foreground">{t.how}</p>
76+
</div>
77+
)}
78+
79+
{error && <Alert variant="error">{error}</Alert>}
80+
81+
{hasDoc && (
82+
<div className="flex flex-wrap gap-2 print:hidden">
83+
<Button variant="secondary" onClick={() => window.print()}><Printer className="h-4 w-4" /> {t.print}</Button>
84+
<Button variant="ghost" onClick={reset}>{t.another}</Button>
85+
</div>
86+
)}
87+
88+
{/* docx-preview renders the document (and its styles) into this container. */}
89+
<div
90+
ref={containerRef}
91+
className={`docx-viewer ${hasDoc ? 'max-h-[78vh] overflow-auto border-2 border-border bg-neutral-200 p-3 dark:bg-neutral-800 print:max-h-none print:overflow-visible print:border-0 print:bg-white print:p-0' : ''}`}
92+
/>
93+
</div>
94+
);
95+
}

src/registry/categories.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const categories: Category[] = [
66
'PDF',
77
'Image',
88
'Files',
9+
'Documents',
910
'Draw',
1011
'Media',
1112
'Network',
@@ -19,6 +20,7 @@ export const categoryColors: Record<Category, string> = {
1920
PDF: 'bg-red-500',
2021
Image: 'bg-green-500',
2122
Files: 'bg-yellow-500',
23+
Documents: 'bg-teal-500',
2224
Draw: 'bg-purple-500',
2325
Media: 'bg-pink-500',
2426
Network: 'bg-cyan-500',
@@ -54,6 +56,7 @@ export const categoryDescriptionsId: Record<Category, string> = {
5456
PDF: 'Kelola PDF secara privat di browser Anda — gabung, pisah, perkecil, konversi, perbaiki, lindungi, dan edit. Dokumen Anda tidak pernah meninggalkan perangkat, jadi berkas rahasia pun tetap aman.',
5557
Image: 'Edit dan konversi gambar di perangkat Anda — ubah ukuran, potong, perkecil, konversi format, hapus latar belakang, tingkatkan resolusi, buramkan wajah, ekstrak teks, dan lainnya. Tanpa unggahan, tanpa tanda air.',
5658
Files: 'Utilitas berkas sehari-hari yang menjaga data Anda tetap lokal — arsipkan, ekstrak, enkripsi, dan periksa berkas langsung di browser tanpa mengirim apa pun ke server.',
59+
Documents: 'Lihat dan konversi dokumen di browser Anda — buka berkas Word, OpenDocument, spreadsheet, dan e-book tanpa aplikasi kantor atau unggahan. Dokumen Anda tidak pernah meninggalkan perangkat.',
5760
Draw: 'Tool menggambar dan membuat diagram sederhana yang berjalan di browser Anda — membuat sketsa, anotasi, dan diagram tanpa akun atau unggahan apa pun.',
5861
Media: 'Utilitas audio dan video privat — konversi, pangkas, rekam, dan transkripsi media sepenuhnya di perangkat Anda. Rekaman Anda tidak pernah meninggalkan browser.',
5962
Network: 'Tool peer-to-peer yang menghubungkan dua perangkat secara langsung untuk mentransfer berkas atau berkomunikasi — data Anda mengalir antar perangkat, bukan melalui server.',
@@ -68,6 +71,7 @@ export const categoryDescriptions: Record<Category, string> = {
6871
PDF: 'Work with PDFs privately in your browser — merge, split, compress, convert, repair, protect and edit. Your documents never leave your device, so even confidential files stay safe.',
6972
Image: 'Edit and convert images on your device — resize, crop, compress, convert formats, remove backgrounds, upscale, blur faces, extract text and more. No uploads, no watermarks.',
7073
Files: 'Everyday file utilities that keep your data local — archive, extract, encrypt and inspect files right in the browser with nothing sent to a server.',
74+
Documents: 'View and convert documents in your browser — open Word, OpenDocument, spreadsheet and e-book files with no office app and no upload. Your documents never leave your device.',
7175
Draw: 'Simple drawing and diagramming tools that run in your browser — sketch, annotate and create diagrams without an account or any upload.',
7276
Media: 'Private audio and video utilities — convert, trim, record and transcribe media entirely on your device using on-device processing. Your recordings never leave your browser.',
7377
Network: 'Peer-to-peer tools that connect two devices directly to transfer files or communicate — your data flows device to device, not through a server.',

src/registry/tool-seo.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ import type { Lang } from '@/i18n/config';
77
* a locale entry is missing. Feeds on-page copy + HowTo/FAQPage structured data.
88
*/
99
const en: Record<string, ToolSeoContent> = {
10+
'docx-viewer': {
11+
title: 'Free DOCX Viewer — Open Word Files Online',
12+
description: 'A free DOCX viewer to open and read Microsoft Word documents in your browser — full layout, tables and images. 100% private; nothing is uploaded.',
13+
intro: 'This free DOCX viewer tool opens Microsoft Word documents right in your browser, rendering the full layout — headings, tables, lists and images — without Word, Google Docs or any account. The file is read on your device and never uploaded.',
14+
howTo: [
15+
'Drop a .docx file (or click to browse) — it is read entirely in your browser.',
16+
'The document renders with its real layout, tables and images.',
17+
'Scroll to read, or use Print / Save as PDF to keep a copy.',
18+
'Nothing is uploaded — the file stays on your device.',
19+
],
20+
faqs: [
21+
{ q: 'Is my document uploaded anywhere?', a: 'No. The .docx is parsed and rendered entirely in your browser with JavaScript. It never leaves your device, so it is safe for confidential documents.' },
22+
{ q: 'Does it support old .doc files?', a: 'No — only the modern .docx format. Open an old .doc in Word or Google Docs and save or export it as .docx first.' },
23+
{ q: 'Can I convert the document to PDF?', a: 'Yes, indirectly: open it here and use Print / Save as PDF in your browser to produce a PDF copy.' },
24+
{ q: 'Will complex formatting look right?', a: 'Most documents — headings, tables, lists, images and basic styling — render faithfully. Very complex layouts or unusual fonts may differ slightly, since it renders with the fonts available in your browser.' },
25+
],
26+
},
1027
'ghost-backup': {
1128
title: 'Free Ghost Blog Backup Tool — Export to Markdown',
1229
description: 'A free tool to convert a Ghost blog export into Markdown or standalone HTML files — one per post, with frontmatter and tags. Runs in your browser; nothing is uploaded.',
@@ -1308,6 +1325,23 @@ const en: Record<string, ToolSeoContent> = {
13081325
};
13091326

13101327
const id: Record<string, ToolSeoContent> = {
1328+
'docx-viewer': {
1329+
title: 'Penampil DOCX Gratis — Buka Berkas Word Online',
1330+
description: 'Penampil DOCX gratis untuk membuka dan membaca dokumen Microsoft Word di browser — tata letak, tabel, dan gambar lengkap. 100% privat; tidak ada yang diunggah.',
1331+
intro: 'Tool penampil DOCX gratis ini membuka dokumen Microsoft Word langsung di browser Anda, menampilkan tata letak lengkap — judul, tabel, daftar, dan gambar — tanpa Word, Google Docs, atau akun apa pun. Berkas dibaca di perangkat Anda dan tidak pernah diunggah.',
1332+
howTo: [
1333+
'Letakkan berkas .docx (atau klik untuk menelusuri) — dibaca sepenuhnya di browser Anda.',
1334+
'Dokumen ditampilkan dengan tata letak, tabel, dan gambar aslinya.',
1335+
'Gulir untuk membaca, atau gunakan Cetak / Simpan PDF untuk menyimpan salinan.',
1336+
'Tidak ada yang diunggah — berkas tetap di perangkat Anda.',
1337+
],
1338+
faqs: [
1339+
{ q: 'Apakah dokumen saya diunggah ke suatu tempat?', a: 'Tidak. Berkas .docx diurai dan ditampilkan sepenuhnya di browser Anda dengan JavaScript. Berkas tidak pernah meninggalkan perangkat, jadi aman untuk dokumen rahasia.' },
1340+
{ q: 'Apakah mendukung berkas .doc lama?', a: 'Tidak — hanya format .docx modern. Buka .doc lama di Word atau Google Docs lalu simpan atau ekspor sebagai .docx terlebih dahulu.' },
1341+
{ q: 'Bisakah saya mengonversi dokumen ke PDF?', a: 'Ya, secara tidak langsung: buka di sini lalu gunakan Cetak / Simpan PDF di browser untuk membuat salinan PDF.' },
1342+
{ q: 'Apakah pemformatan rumit akan tampil dengan benar?', a: 'Sebagian besar dokumen — judul, tabel, daftar, gambar, dan gaya dasar — tampil dengan setia. Tata letak yang sangat rumit atau font tidak biasa mungkin sedikit berbeda, karena ditampilkan dengan font yang tersedia di browser Anda.' },
1343+
],
1344+
},
13111345
'ghost-backup': {
13121346
title: 'Tool Cadangan Blog Ghost Gratis — Ekspor ke Markdown',
13131347
description: 'Tool gratis untuk mengubah ekspor blog Ghost menjadi berkas Markdown atau HTML mandiri — satu per pos, dengan frontmatter dan tag. Berjalan di browser; tidak ada yang diunggah.',

src/registry/tools.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,17 @@ export const tools: ToolDef[] = [
157157
load: () => import('@/islands/dev/GhostBackup'),
158158
status: 'beta'
159159
},
160+
{
161+
id: 'docx-viewer',
162+
name: 'Word (DOCX) Viewer',
163+
category: 'Documents',
164+
route: '/tools/docx-viewer',
165+
keywords: ['docx', 'word', 'viewer', 'open', 'read', 'document', 'office', 'preview', 'doc'],
166+
icon: FileText,
167+
summary: 'Open and read Word .docx files in your browser',
168+
load: () => import('@/islands/documents/DocxViewer'),
169+
status: 'beta'
170+
},
160171
{
161172
id: 'markdown',
162173
name: 'Markdown Preview',

src/types/tool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { LucideIcon } from 'lucide-react';
22

3-
export type Category = 'Dev' | 'PDF' | 'Image' | 'Files' | 'Draw' | 'Media' | 'Network' | 'Maps' | 'Legacy' | 'Playground';
3+
export type Category = 'Dev' | 'PDF' | 'Image' | 'Files' | 'Documents' | 'Draw' | 'Media' | 'Network' | 'Maps' | 'Legacy' | 'Playground';
44

55
export interface AssetRef {
66
url: string;

0 commit comments

Comments
 (0)