Initial commit: TypoGenie - Markdown to Word document converter
This commit is contained in:
263
components/StyleSelector.tsx
Normal file
263
components/StyleSelector.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
||||
import { TYPOGRAPHY_STYLES } from '../constants';
|
||||
import { StyleOption, PaperSize, StyleCategory } from '../types';
|
||||
import { Check, Type, Printer, Search } from 'lucide-react';
|
||||
|
||||
interface StyleSelectorProps {
|
||||
selectedStyle: string | null;
|
||||
onSelectStyle: (id: string) => void;
|
||||
selectedPaperSize: PaperSize;
|
||||
onSelectPaperSize: (size: PaperSize) => void;
|
||||
onGenerate: () => void;
|
||||
}
|
||||
|
||||
const SAMPLE_CONTENT = `
|
||||
<h1>The Art of Typography</h1>
|
||||
<p>Typography is the art and technique of arranging type to make written language legible, readable, and appealing when displayed. The arrangement of type involves selecting typefaces, point sizes, line lengths, line-spacing, and letter-spacing.</p>
|
||||
|
||||
<h2>1. Visual Hierarchy</h2>
|
||||
<p>Visual hierarchy enables the reader to understand the importance of different sections. By using size, weight, and color, we guide the eye through the document in a logical flow.</p>
|
||||
|
||||
<blockquote>
|
||||
"Design is not just what it looks like and feels like. Design is how it works."
|
||||
</blockquote>
|
||||
|
||||
<h2>2. Key Elements</h2>
|
||||
<ul>
|
||||
<li><strong>Typeface:</strong> The design of the letters.</li>
|
||||
<li><strong>Contrast:</strong> Distinguishing elements effectively.</li>
|
||||
<li><strong>Consistency:</strong> Maintaining a coherent structure.</li>
|
||||
</ul>
|
||||
|
||||
<p>Good typography is invisible. It should not distract the reader but rather enhance the reading experience, ensuring the message is delivered clearly and effectively.</p>
|
||||
`;
|
||||
|
||||
export const StyleSelector: React.FC<StyleSelectorProps> = ({
|
||||
selectedStyle,
|
||||
onSelectStyle,
|
||||
selectedPaperSize,
|
||||
onSelectPaperSize,
|
||||
onGenerate
|
||||
}) => {
|
||||
const [activeCategory, setActiveCategory] = useState<StyleCategory | 'All'>('All');
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
// Dynamically extract categories from styles and sort them
|
||||
const categories = useMemo(() => {
|
||||
const cats = new Set(TYPOGRAPHY_STYLES.map(s => s.category));
|
||||
return Array.from(cats).sort();
|
||||
}, []);
|
||||
|
||||
const filteredStyles = useMemo(() => {
|
||||
let styles = TYPOGRAPHY_STYLES;
|
||||
if (activeCategory !== 'All') {
|
||||
styles = TYPOGRAPHY_STYLES.filter(style => style.category === activeCategory);
|
||||
}
|
||||
// Always sort alphabetically by name
|
||||
return [...styles].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [activeCategory]);
|
||||
|
||||
const currentStyleObj = useMemo(() =>
|
||||
TYPOGRAPHY_STYLES.find(s => s.id === selectedStyle),
|
||||
[selectedStyle]);
|
||||
|
||||
// Update preview when style changes
|
||||
useEffect(() => {
|
||||
if (!iframeRef.current || !currentStyleObj) return;
|
||||
|
||||
const doc = iframeRef.current.contentDocument;
|
||||
if (doc) {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="${currentStyleObj.googleFontsImport}" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
margin: 0;
|
||||
padding: 40px;
|
||||
/* Inject default body color from config to ensure lists/quotes inherit correctly */
|
||||
color: #${currentStyleObj.wordConfig.body.color};
|
||||
font-family: sans-serif;
|
||||
/* Apply Selected Style CSS */
|
||||
${currentStyleObj.previewCss}
|
||||
}
|
||||
/* Scrollbar styling for the preview iframe */
|
||||
::-webkit-scrollbar { width: 8px; }
|
||||
::-webkit-scrollbar-track { background: #f1f1f1; }
|
||||
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${SAMPLE_CONTENT}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
doc.open();
|
||||
doc.write(html);
|
||||
doc.close();
|
||||
}
|
||||
}, [currentStyleObj]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-[calc(100vh-140px)] min-h-[600px] flex flex-col gap-6 max-w-[1600px] mx-auto">
|
||||
|
||||
{/* Top Controls */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 px-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-indigo-500/10 rounded-lg text-indigo-400">
|
||||
<Type size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-zinc-100">Select Typography Style</h2>
|
||||
<p className="text-zinc-400 text-sm">Browse {TYPOGRAPHY_STYLES.length} professional templates</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 bg-zinc-900/50 p-1.5 rounded-xl border border-zinc-800/50">
|
||||
<div className="flex items-center gap-2 text-zinc-400 px-3">
|
||||
<Printer size={16} />
|
||||
<span className="text-xs font-semibold uppercase tracking-wider">Format</span>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{(['Letter', 'A4'] as PaperSize[]).map((size) => (
|
||||
<button
|
||||
key={size}
|
||||
onClick={() => onSelectPaperSize(size)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
|
||||
selectedPaperSize === size
|
||||
? 'bg-zinc-700 text-white shadow-sm'
|
||||
: 'text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800'
|
||||
}`}
|
||||
>
|
||||
{size}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Split Layout */}
|
||||
<div className="flex-1 grid grid-cols-1 lg:grid-cols-12 gap-6 min-h-0">
|
||||
|
||||
{/* LEFT COLUMN: Style List */}
|
||||
<div className="lg:col-span-4 flex flex-col gap-4 min-h-0 bg-zinc-900/30 rounded-2xl border border-zinc-800/50 overflow-hidden">
|
||||
|
||||
{/* Category Filter Tabs */}
|
||||
<div className="p-4 border-b border-zinc-800/50 overflow-x-auto no-scrollbar">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setActiveCategory('All')}
|
||||
className={`px-3 py-1.5 rounded-full text-xs font-semibold whitespace-nowrap transition-all ${
|
||||
activeCategory === 'All'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200'
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{categories.map(cat => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setActiveCategory(cat)}
|
||||
className={`px-3 py-1.5 rounded-full text-xs font-semibold whitespace-nowrap transition-all ${
|
||||
activeCategory === cat
|
||||
? 'bg-indigo-500 text-white shadow-lg shadow-indigo-500/20'
|
||||
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200'
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scrollable List */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3 custom-scrollbar">
|
||||
{filteredStyles.map((style) => (
|
||||
<div
|
||||
key={style.id}
|
||||
onClick={() => onSelectStyle(style.id)}
|
||||
className={`p-4 rounded-xl border transition-all cursor-pointer group relative
|
||||
${selectedStyle === style.id
|
||||
? 'border-indigo-500 bg-indigo-500/10 shadow-[0_0_15px_rgba(99,102,241,0.15)]'
|
||||
: 'border-zinc-800 bg-zinc-900/40 hover:border-zinc-700 hover:bg-zinc-800'}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-1">
|
||||
<h3 className={`font-bold text-sm ${selectedStyle === style.id ? 'text-white' : 'text-zinc-300'}`}>
|
||||
{style.name}
|
||||
</h3>
|
||||
{selectedStyle === style.id && (
|
||||
<div className="bg-indigo-500 rounded-full p-0.5">
|
||||
<Check size={12} className="text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500 line-clamp-2 leading-relaxed mb-2">{style.description}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-[10px] uppercase font-mono tracking-wider px-1.5 py-0.5 rounded
|
||||
${selectedStyle === style.id ? 'bg-indigo-500/20 text-indigo-300' : 'bg-zinc-800 text-zinc-500'}`}>
|
||||
{style.category}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT COLUMN: Preview Window */}
|
||||
<div className="lg:col-span-8 flex flex-col min-h-0 bg-zinc-950 rounded-2xl border border-zinc-800 shadow-2xl relative overflow-hidden">
|
||||
|
||||
{/* Preview Header */}
|
||||
<div className="h-12 border-b border-zinc-800 bg-zinc-900/50 flex items-center px-4 justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500/20 border border-red-500/50"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-amber-500/20 border border-amber-500/50"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-emerald-500/20 border border-emerald-500/50"></div>
|
||||
</div>
|
||||
<div className="h-4 w-px bg-zinc-800 mx-2"></div>
|
||||
<span className="text-xs text-zinc-500 font-medium">Live Preview</span>
|
||||
</div>
|
||||
{currentStyleObj && (
|
||||
<span className="text-xs text-indigo-400 font-mono">{currentStyleObj.name}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Iframe Container */}
|
||||
<div className="flex-1 relative bg-zinc-900/50">
|
||||
{selectedStyle ? (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
className="w-full h-full border-0 block"
|
||||
title="Style Preview"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-zinc-600">
|
||||
<Search size={48} className="mb-4 opacity-20" />
|
||||
<p className="text-lg font-medium">Select a style to preview</p>
|
||||
<p className="text-sm">Choose from the list on the left</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom Action Bar */}
|
||||
<div className="p-4 border-t border-zinc-800 bg-zinc-900/50 flex justify-end items-center gap-4">
|
||||
<button
|
||||
onClick={onGenerate}
|
||||
disabled={!selectedStyle}
|
||||
className="flex items-center gap-2 px-6 py-3 bg-indigo-600 hover:bg-indigo-500 text-white font-bold rounded-xl transition-all shadow-lg hover:shadow-indigo-500/25 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span>Apply Style & Convert</span>
|
||||
<Printer size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user