Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"astro": "^4.16.19",
"cmdk": "^0.2.1",
"comlink": "^4.4.2",
"docx-preview": "^0.4.0",
"dompurify": "^3.4.12",
"fast-xml-parser": "^5.10.0",
"fflate": "^0.8.3",
Expand Down
2 changes: 1 addition & 1 deletion scripts/generate-og.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const fonts = [
];

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

Expand Down
95 changes: 95 additions & 0 deletions src/islands/documents/DocxViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useRef, useState } from 'react';
import { FileText, Printer } from 'lucide-react';
import { Dropzone } from '@/components/ui/Dropzone';
import { Button } from '@/components/ui/Button';
import { Alert } from '@/components/ui/Alert';
import type { Lang } from '@/i18n/config';

const TR: Record<Lang, {
intro: string; drop: string; dropSub: string; how: string;
opening: string; another: string; print: string; errRead: string;
}> = {
en: {
intro: 'Open and read a Word document (.docx) right here — full layout, tables and images. It is rendered on your device; nothing is uploaded.',
drop: 'Drop a Word document (.docx)', dropSub: 'Rendered on your device — no upload.',
how: 'Older .doc (binary) files aren’t supported — save as .docx in Word/Google Docs first.',
opening: 'Rendering…', another: 'Open another', print: 'Print / Save as PDF',
errRead: 'Could not open this document — is it a valid .docx file?',
},
id: {
intro: 'Buka dan baca dokumen Word (.docx) langsung di sini — tata letak, tabel, dan gambar lengkap. Ditampilkan di perangkat Anda; tidak ada yang diunggah.',
drop: 'Letakkan dokumen Word (.docx)', dropSub: 'Ditampilkan di perangkat Anda — tanpa unggahan.',
how: 'Berkas .doc lama (biner) tidak didukung — simpan sebagai .docx di Word/Google Docs terlebih dahulu.',
opening: 'Menampilkan…', another: 'Buka yang lain', print: 'Cetak / Simpan PDF',
errRead: 'Tidak dapat membuka dokumen ini — apakah berkas .docx yang valid?',
},
};

export default function DocxViewer({ lang = 'en' }: { lang?: Lang }) {
const t = TR[lang] ?? TR.en;
const containerRef = useRef<HTMLDivElement>(null);
const [hasDoc, setHasDoc] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');

const onDrop = async (files: File[]) => {
const f = files[0];
if (!f) return;
setError('');
setBusy(true);
try {
const buf = await f.arrayBuffer();
const { renderAsync } = await import('docx-preview');
const container = containerRef.current!;
container.innerHTML = '';
await renderAsync(buf, container, undefined, {
className: 'docx', inWrapper: true, breakPages: true, ignoreLastRenderedPageBreak: false,
});
setHasDoc(true);
} catch {
setError(t.errRead);
setHasDoc(false);
} finally {
setBusy(false);
}
};

const reset = () => {
if (containerRef.current) containerRef.current.innerHTML = '';
setHasDoc(false);
setError('');
};

return (
<div className="space-y-4">
<p className="text-sm text-muted-foreground print:hidden">{t.intro}</p>

{!hasDoc && (
<div className="print:hidden">
<Dropzone onDrop={onDrop} accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document" multiple={false}>
<div className="space-y-1">
<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>
<p className="text-sm text-muted-foreground">{t.dropSub}</p>
</div>
</Dropzone>
<p className="mt-2 text-xs text-muted-foreground">{t.how}</p>
</div>
)}

{error && <Alert variant="error">{error}</Alert>}

{hasDoc && (
<div className="flex flex-wrap gap-2 print:hidden">
<Button variant="secondary" onClick={() => window.print()}><Printer className="h-4 w-4" /> {t.print}</Button>
<Button variant="ghost" onClick={reset}>{t.another}</Button>
</div>
)}

{/* docx-preview renders the document (and its styles) into this container. */}
<div
ref={containerRef}
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' : ''}`}
/>
</div>
);
}
4 changes: 4 additions & 0 deletions src/registry/categories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const categories: Category[] = [
'PDF',
'Image',
'Files',
'Documents',
'Draw',
'Media',
'Network',
Expand All @@ -19,6 +20,7 @@ export const categoryColors: Record<Category, string> = {
PDF: 'bg-red-500',
Image: 'bg-green-500',
Files: 'bg-yellow-500',
Documents: 'bg-teal-500',
Draw: 'bg-purple-500',
Media: 'bg-pink-500',
Network: 'bg-cyan-500',
Expand Down Expand Up @@ -54,6 +56,7 @@ export const categoryDescriptionsId: Record<Category, string> = {
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.',
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.',
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.',
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.',
Draw: 'Tool menggambar dan membuat diagram sederhana yang berjalan di browser Anda — membuat sketsa, anotasi, dan diagram tanpa akun atau unggahan apa pun.',
Media: 'Utilitas audio dan video privat — konversi, pangkas, rekam, dan transkripsi media sepenuhnya di perangkat Anda. Rekaman Anda tidak pernah meninggalkan browser.',
Network: 'Tool peer-to-peer yang menghubungkan dua perangkat secara langsung untuk mentransfer berkas atau berkomunikasi — data Anda mengalir antar perangkat, bukan melalui server.',
Expand All @@ -68,6 +71,7 @@ export const categoryDescriptions: Record<Category, string> = {
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.',
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.',
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.',
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.',
Draw: 'Simple drawing and diagramming tools that run in your browser — sketch, annotate and create diagrams without an account or any upload.',
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.',
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.',
Expand Down
34 changes: 34 additions & 0 deletions src/registry/tool-seo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ import type { Lang } from '@/i18n/config';
* a locale entry is missing. Feeds on-page copy + HowTo/FAQPage structured data.
*/
const en: Record<string, ToolSeoContent> = {
'docx-viewer': {
title: 'Free DOCX Viewer — Open Word Files Online',
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.',
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.',
howTo: [
'Drop a .docx file (or click to browse) — it is read entirely in your browser.',
'The document renders with its real layout, tables and images.',
'Scroll to read, or use Print / Save as PDF to keep a copy.',
'Nothing is uploaded — the file stays on your device.',
],
faqs: [
{ 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.' },
{ 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.' },
{ 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.' },
{ 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.' },
],
},
'ghost-backup': {
title: 'Free Ghost Blog Backup Tool — Export to Markdown',
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.',
Expand Down Expand Up @@ -1308,6 +1325,23 @@ const en: Record<string, ToolSeoContent> = {
};

const id: Record<string, ToolSeoContent> = {
'docx-viewer': {
title: 'Penampil DOCX Gratis — Buka Berkas Word Online',
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.',
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.',
howTo: [
'Letakkan berkas .docx (atau klik untuk menelusuri) — dibaca sepenuhnya di browser Anda.',
'Dokumen ditampilkan dengan tata letak, tabel, dan gambar aslinya.',
'Gulir untuk membaca, atau gunakan Cetak / Simpan PDF untuk menyimpan salinan.',
'Tidak ada yang diunggah — berkas tetap di perangkat Anda.',
],
faqs: [
{ 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.' },
{ 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.' },
{ 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.' },
{ 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.' },
],
},
'ghost-backup': {
title: 'Tool Cadangan Blog Ghost Gratis — Ekspor ke Markdown',
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.',
Expand Down
11 changes: 11 additions & 0 deletions src/registry/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,17 @@ export const tools: ToolDef[] = [
load: () => import('@/islands/dev/GhostBackup'),
status: 'beta'
},
{
id: 'docx-viewer',
name: 'Word (DOCX) Viewer',
category: 'Documents',
route: '/tools/docx-viewer',
keywords: ['docx', 'word', 'viewer', 'open', 'read', 'document', 'office', 'preview', 'doc'],
icon: FileText,
summary: 'Open and read Word .docx files in your browser',
load: () => import('@/islands/documents/DocxViewer'),
status: 'beta'
},
{
id: 'markdown',
name: 'Markdown Preview',
Expand Down
2 changes: 1 addition & 1 deletion src/types/tool.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { LucideIcon } from 'lucide-react';

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

export interface AssetRef {
url: string;
Expand Down
Loading