Features: - Magazine-style layout with CSS Grid (2-column + sidebar) - ICW NextSpread design system (colors, typography, animations) - Adaptive animation timing based on scroll speed - URL query parameter support (?parse=https://url/file.md) - Drag-and-drop file upload - Syntax highlighting (VS Code Dark+ theme) - Full-bleed hero section with gradient overlay - Responsive design (mobile-first, 44px touch targets) - Static export ready for StackCP deployment Tech Stack: - Next.js 14 (static export) - React 18 + TypeScript - Framer Motion (animations) - markdown-it + highlight.js - Tailwind CSS Design Ported From: - ICW NextSpread property pages (icantwait.ca) - useAdaptiveDuration, useScrollSpeed animation hooks - Webflow-style easing curves - Gallery staggered reveal patterns Deployment: - Configured for digital-lab.ca/infrafabric/IF.docs.md2html - .htaccess with SPA routing and CORS headers - Static files in /out/ directory 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect } from 'react';
|
||
import { useSearchParams } from 'next/navigation';
|
||
import { MarkdownViewer } from '@/components/MarkdownViewer';
|
||
import { FileUploader } from '@/components/FileUploader';
|
||
|
||
export function ViewerContent() {
|
||
const [markdown, setMarkdown] = useState('');
|
||
const [title, setTitle] = useState('');
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState('');
|
||
const searchParams = useSearchParams();
|
||
|
||
// Load markdown from URL query parameter ?parse=https://url/file.md
|
||
useEffect(() => {
|
||
const parseUrl = searchParams.get('parse');
|
||
if (parseUrl) {
|
||
setLoading(true);
|
||
fetch(parseUrl)
|
||
.then((res) => {
|
||
if (!res.ok) throw new Error(`Failed to fetch: ${res.statusText}`);
|
||
return res.text();
|
||
})
|
||
.then((content) => {
|
||
// Extract title from first H1
|
||
const h1Match = content.match(/^#\s+(.+)$/m);
|
||
const extractedTitle = h1Match ? h1Match[1] : 'Document';
|
||
setTitle(extractedTitle);
|
||
setMarkdown(content);
|
||
setLoading(false);
|
||
})
|
||
.catch((err) => {
|
||
setError(`Error loading document: ${err.message}`);
|
||
setLoading(false);
|
||
});
|
||
}
|
||
}, [searchParams]);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center">
|
||
<div className="text-center">
|
||
<div className="inline-block w-8 h-8 border-4 border-primary-500 border-t-transparent rounded-full animate-spin mb-4"></div>
|
||
<p className="regular-m text-neutral-600">Loading document...</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center p-6">
|
||
<div className="max-w-lg text-center">
|
||
<div className="text-6xl mb-4">⚠️</div>
|
||
<h2 className="h3 mb-2">Error Loading Document</h2>
|
||
<p className="regular-m text-neutral-600 mb-6">{error}</p>
|
||
<button
|
||
onClick={() => { setError(''); }}
|
||
className="inline-flex items-center gap-2 px-6 py-3 bg-neutral-900 text-white rounded-full hover:bg-neutral-800 transition-all"
|
||
>
|
||
Try Again
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<main className="min-h-screen bg-neutral-50">
|
||
{!markdown ? (
|
||
<FileUploader onMarkdownLoad={setMarkdown} onTitleExtract={setTitle} />
|
||
) : (
|
||
<MarkdownViewer markdown={markdown} title={title} onReset={() => { setMarkdown(''); setTitle(''); }} />
|
||
)}
|
||
</main>
|
||
);
|
||
}
|