Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Arc Project Starter</title> | |
| <script src="https://unpkg.com/react@18/umd/react.development.js"></script> | |
| <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> | |
| <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script> | |
| tailwind.config = { | |
| content: ["*"], | |
| theme: { | |
| extend: {} | |
| } | |
| } | |
| </script> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <script type="text/babel"> | |
| const { useState } = React; | |
| // Lucide React icons as simple SVG components | |
| const Copy = ({ className }) => ( | |
| <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> | |
| <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> | |
| </svg> | |
| ); | |
| const BookOpen = ({ className }) => ( | |
| <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path> | |
| <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path> | |
| </svg> | |
| ); | |
| const FileText = ({ className }) => ( | |
| <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> | |
| <polyline points="14,2 14,8 20,8"></polyline> | |
| <line x1="16" y1="13" x2="8" y2="13"></line> | |
| <line x1="16" y1="17" x2="8" y2="17"></line> | |
| <polyline points="10,9 9,9 8,9"></polyline> | |
| </svg> | |
| ); | |
| const Grid = ({ className }) => ( | |
| <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <rect x="3" y="3" width="7" height="7"></rect> | |
| <rect x="14" y="3" width="7" height="7"></rect> | |
| <rect x="14" y="14" width="7" height="7"></rect> | |
| <rect x="3" y="14" width="7" height="7"></rect> | |
| </svg> | |
| ); | |
| const Calculator = ({ className }) => ( | |
| <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <rect x="4" y="2" width="16" height="20" rx="2"></rect> | |
| <line x1="8" y1="6" x2="16" y2="6"></line> | |
| <line x1="8" y1="10" x2="16" y2="10"></line> | |
| <line x1="8" y1="14" x2="16" y2="14"></line> | |
| <line x1="8" y1="18" x2="16" y2="18"></line> | |
| </svg> | |
| ); | |
| const RefreshCw = ({ className }) => ( | |
| <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <polyline points="23 4 23 10 17 10"></polyline> | |
| <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path> | |
| </svg> | |
| ); | |
| const Download = ({ className }) => ( | |
| <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
| <polyline points="7 10 12 15 17 10"></polyline> | |
| <line x1="12" y1="15" x2="12" y2="3"></line> | |
| </svg> | |
| ); | |
| // Simple Card components | |
| const Card = ({ children, className = "" }) => ( | |
| <div className={`rounded-lg border bg-card text-card-foreground shadow-sm ${className}`}> | |
| {children} | |
| </div> | |
| ); | |
| const CardContent = ({ children, className = "" }) => ( | |
| <div className={`p-6 pt-0 ${className}`}> | |
| {children} | |
| </div> | |
| ); | |
| const CardHeader = ({ children, className = "" }) => ( | |
| <div className={`flex flex-col space-y-1.5 p-6 ${className}`}> | |
| {children} | |
| </div> | |
| ); | |
| const CardTitle = ({ children, className = "" }) => ( | |
| <h3 className={`text-2xl font-semibold leading-none tracking-tight ${className}`}> | |
| {children} | |
| </h3> | |
| ); | |
| // Simple Button component | |
| const Button = ({ children, onClick, variant = "default", className = "" }) => { | |
| const baseClasses = "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 px-4 py-2"; | |
| const variantClasses = variant === "outline" | |
| ? "border border-input bg-background hover:bg-accent hover:text-accent-foreground" | |
| : "bg-primary text-primary-foreground hover:bg-primary/90"; | |
| return ( | |
| <button | |
| className={`${baseClasses} ${variantClasses} ${className}`} | |
| onClick={onClick} | |
| > | |
| {children} | |
| </button> | |
| ); | |
| }; | |
| function ArcProjectStarter() { | |
| const [books, setBooks] = useState(1); | |
| const [chapters, setChapters] = useState(10); | |
| const [pagesRange, setPagesRange] = useState([15, 25]); | |
| const [panelsRange, setPanelsRange] = useState([4, 6]); | |
| const [excludePagesMin, setExcludePagesMin] = useState(false); | |
| const [excludePanelsMin, setExcludePanelsMin] = useState(false); | |
| const [context, setContext] = useState("A fantasy adventure manga about a young mage discovering ancient magic"); | |
| const [outputTitle, setOutputTitle] = useState("My Manga Project"); | |
| const [contextTitle, setContextTitle] = useState("Story Context"); | |
| const [copySuccess, setCopySuccess] = useState(false); | |
| // Adjusted ranges based on checkboxes | |
| const effectivePagesRange = excludePagesMin ? [pagesRange[1], pagesRange[1]] : pagesRange; | |
| const effectivePanelsRange = excludePanelsMin ? [panelsRange[1], panelsRange[1]] : panelsRange; | |
| // Calculation for page and panel estimates | |
| const totalPagesMin = books * chapters * effectivePagesRange[0]; | |
| const totalPagesMax = books * chapters * effectivePagesRange[1]; | |
| const totalPanelsMin = totalPagesMin * effectivePanelsRange[0]; | |
| const totalPanelsMax = totalPagesMax * effectivePanelsRange[1]; | |
| // Explicit total pages of book range string | |
| const totalPagesOfBookRange = `${totalPagesMin}${totalPagesMin !== totalPagesMax ? ` - ${totalPagesMax}` : ''}`; | |
| const outputPrompt = `${outputTitle ? `# ${outputTitle}` : ''} | |
| ${contextTitle ? `## ${contextTitle}` : ''} | |
| "${context}" | |
| ## Project Specifications | |
| - **Books:** ${books} | |
| - **Chapters per Book:** ${chapters} | |
| - **Pages per Chapter:** ${effectivePagesRange[0]}${effectivePagesRange[0] !== effectivePagesRange[1] ? ` - ${effectivePagesRange[1]}` : ''} | |
| - **Total Pages Range:** ${totalPagesMin}${totalPagesMin !== totalPagesMax ? ` - ${totalPagesMax}` : ''} | |
| - **Panels per Page:** ${effectivePanelsRange[0]}${effectivePanelsRange[0] !== effectivePanelsRange[1] ? ` - ${effectivePanelsRange[1]}` : ''} | |
| - **Total Panels Range:** ${totalPanelsMin}${totalPanelsMin !== totalPanelsMax ? ` - ${totalPanelsMax}` : ''} | |
| - **Total Pages of Complete Work:** ${totalPagesOfBookRange}`; | |
| const copyToClipboard = async () => { | |
| try { | |
| await navigator.clipboard.writeText(outputPrompt); | |
| setCopySuccess(true); | |
| setTimeout(() => setCopySuccess(false), 2000); | |
| } catch (err) { | |
| console.error('Failed to copy text: ', err); | |
| } | |
| }; | |
| const downloadAsFile = () => { | |
| const blob = new Blob([outputPrompt], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `${outputTitle.replace(/\s+/g, '_')}_project_specs.txt`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }; | |
| const resetForm = () => { | |
| setBooks(1); | |
| setChapters(10); | |
| setPagesRange([15, 25]); | |
| setPanelsRange([4, 6]); | |
| setExcludePagesMin(false); | |
| setExcludePanelsMin(false); | |
| setContext("A fantasy adventure manga about a young mage discovering ancient magic"); | |
| setOutputTitle("My Manga Project"); | |
| setContextTitle("Story Context"); | |
| }; | |
| // Statistics cards data | |
| const stats = [ | |
| { | |
| label: "Total Pages", | |
| value: totalPagesOfBookRange, | |
| icon: FileText, | |
| color: "text-blue-600", | |
| bgColor: "bg-blue-50" | |
| }, | |
| { | |
| label: "Total Panels", | |
| value: `${totalPanelsMin}${totalPanelsMin !== totalPanelsMax ? ` - ${totalPanelsMax}` : ''}`, | |
| icon: Grid, | |
| color: "text-purple-600", | |
| bgColor: "bg-purple-50" | |
| }, | |
| { | |
| label: "Avg Pages/Chapter", | |
| value: `${effectivePagesRange[0]}${effectivePagesRange[0] !== effectivePagesRange[1] ? ` - ${effectivePagesRange[1]}` : ''}`, | |
| icon: BookOpen, | |
| color: "text-green-600", | |
| bgColor: "bg-green-50" | |
| } | |
| ]; | |
| return ( | |
| <div className="min-h-screen bg-gradient-to-br from-indigo-100 via-purple-50 to-pink-100 p-6"> | |
| <div className="max-w-7xl mx-auto"> | |
| {/* Header */} | |
| <div className="text-center mb-8"> | |
| <h1 className="text-4xl font-bold text-gray-800 mb-2">Arc Project Starter</h1> | |
| <p className="text-gray-600">Plan and calculate your manga or comic project specifications</p> | |
| </div> | |
| {/* Statistics Cards */} | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8"> | |
| {stats.map((stat, index) => ( | |
| <Card key={index} className={`${stat.bgColor} border-0 shadow-lg`}> | |
| <CardContent className="p-4"> | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <p className="text-sm font-medium text-gray-600">{stat.label}</p> | |
| <p className={`text-2xl font-bold ${stat.color}`}>{stat.value}</p> | |
| </div> | |
| <stat.icon className={`h-8 w-8 ${stat.color}`} /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ))} | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> | |
| {/* Left Panel - Configuration */} | |
| <Card className="bg-white/80 backdrop-blur-sm shadow-xl border-0"> | |
| <CardHeader className="bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-t-lg"> | |
| <CardTitle className="flex items-center gap-2"> | |
| <Calculator className="h-5 w-5" /> | |
| Project Configuration | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent className="p-6 space-y-6"> | |
| {/* Basic Structure */} | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 mb-2">Books</label> | |
| <input | |
| type="number" | |
| min="1" | |
| max="100" | |
| value={books} | |
| onChange={(e) => setBooks(Math.max(1, parseInt(e.target.value) || 1))} | |
| className="w-full border-2 border-gray-200 rounded-lg p-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 mb-2">Chapters per Book</label> | |
| <input | |
| type="number" | |
| min="1" | |
| max="1000" | |
| value={chapters} | |
| onChange={(e) => setChapters(Math.max(1, parseInt(e.target.value) || 1))} | |
| className="w-full border-2 border-gray-200 rounded-lg p-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all" | |
| /> | |
| </div> | |
| </div> | |
| {/* Pages Range */} | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 mb-2">Pages per Chapter Range</label> | |
| <div className="flex gap-3 items-center"> | |
| <input | |
| type="number" | |
| min="1" | |
| max="1000" | |
| value={pagesRange[0]} | |
| onChange={(e) => setPagesRange([Math.max(1, parseInt(e.target.value) || 1), Math.max(pagesRange[0], pagesRange[1])])} | |
| className="flex-1 border-2 border-gray-200 rounded-lg p-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all" | |
| placeholder="Min" | |
| /> | |
| <span className="text-gray-500 font-medium">to</span> | |
| <input | |
| type="number" | |
| min={pagesRange[0]} | |
| max="1000" | |
| value={pagesRange[1]} | |
| onChange={(e) => setPagesRange([pagesRange[0], Math.max(pagesRange[0], parseInt(e.target.value) || pagesRange[0])])} | |
| className="flex-1 border-2 border-gray-200 rounded-lg p-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all" | |
| placeholder="Max" | |
| /> | |
| </div> | |
| <label className="flex items-center mt-2 text-sm"> | |
| <input | |
| type="checkbox" | |
| checked={excludePagesMin} | |
| onChange={(e) => setExcludePagesMin(e.target.checked)} | |
| className="mr-2 w-4 h-4 text-blue-600" | |
| /> | |
| <span className="text-gray-600">Use only maximum value</span> | |
| </label> | |
| </div> | |
| {/* Panels Range */} | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 mb-2">Panels per Page Range</label> | |
| <div className="flex gap-3 items-center"> | |
| <input | |
| type="number" | |
| min="1" | |
| max="50" | |
| value={panelsRange[0]} | |
| onChange={(e) => setPanelsRange([Math.max(1, parseInt(e.target.value) || 1), Math.max(panelsRange[0], panelsRange[1])])} | |
| className="flex-1 border-2 border-gray-200 rounded-lg p-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all" | |
| placeholder="Min" | |
| /> | |
| <span className="text-gray-500 font-medium">to</span> | |
| <input | |
| type="number" | |
| min={panelsRange[0]} | |
| max="50" | |
| value={panelsRange[1]} | |
| onChange={(e) => setPanelsRange([panelsRange[0], Math.max(panelsRange[0], parseInt(e.target.value) || panelsRange[0])])} | |
| className="flex-1 border-2 border-gray-200 rounded-lg p-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all" | |
| placeholder="Max" | |
| /> | |
| </div> | |
| <label className="flex items-center mt-2 text-sm"> | |
| <input | |
| type="checkbox" | |
| checked={excludePanelsMin} | |
| onChange={(e) => setExcludePanelsMin(e.target.checked)} | |
| className="mr-2 w-4 h-4 text-blue-600" | |
| /> | |
| <span className="text-gray-600">Use only maximum value</span> | |
| </label> | |
| </div> | |
| {/* Reset Button */} | |
| <Button | |
| onClick={resetForm} | |
| variant="outline" | |
| className="w-full border-2 border-gray-300 hover:bg-gray-50" | |
| > | |
| <RefreshCw className="h-4 w-4 mr-2" /> | |
| Reset to Defaults | |
| </Button> | |
| </CardContent> | |
| </Card> | |
| {/* Right Panel - Output */} | |
| <Card className="bg-white/80 backdrop-blur-sm shadow-xl border-0"> | |
| <CardHeader className="bg-gradient-to-r from-green-500 to-teal-600 text-white rounded-t-lg"> | |
| <CardTitle className="flex items-center gap-2"> | |
| <FileText className="h-5 w-5" /> | |
| Project Output | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent className="p-6 space-y-6"> | |
| {/* Title Inputs */} | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 mb-2">Project Title</label> | |
| <input | |
| type="text" | |
| value={outputTitle} | |
| onChange={(e) => setOutputTitle(e.target.value)} | |
| className="w-full border-2 border-gray-200 rounded-lg p-3 focus:border-green-500 focus:ring-2 focus:ring-green-200 transition-all" | |
| placeholder="Enter project title..." | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 mb-2">Context Section Title</label> | |
| <input | |
| type="text" | |
| value={contextTitle} | |
| onChange={(e) => setContextTitle(e.target.value)} | |
| className="w-full border-2 border-gray-200 rounded-lg p-3 focus:border-green-500 focus:ring-2 focus:ring-green-200 transition-all" | |
| placeholder="Enter context section title..." | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 mb-2">Project Context</label> | |
| <textarea | |
| value={context} | |
| onChange={(e) => setContext(e.target.value)} | |
| className="w-full border-2 border-gray-200 rounded-lg p-3 focus:border-green-500 focus:ring-2 focus:ring-green-200 transition-all resize-none" | |
| rows={4} | |
| placeholder="Describe your project context, story, theme..." | |
| /> | |
| </div> | |
| {/* Generated Output */} | |
| <div className="bg-gradient-to-r from-gray-50 to-gray-100 rounded-xl p-4 border-2 border-gray-200"> | |
| <h3 className="text-lg font-semibold mb-3 text-gray-800">Generated Project Specification</h3> | |
| <pre className="whitespace-pre-wrap text-sm bg-white p-4 rounded-lg border shadow-inner font-mono text-gray-700 max-h-64 overflow-y-auto"> | |
| {outputPrompt} | |
| </pre> | |
| {/* Action Buttons */} | |
| <div className="flex gap-3 mt-4"> | |
| <Button | |
| onClick={copyToClipboard} | |
| className={`flex-1 transition-all ${copySuccess ? 'bg-green-600 hover:bg-green-700' : 'bg-blue-600 hover:bg-blue-700'} text-white`} | |
| > | |
| <Copy className="h-4 w-4 mr-2" /> | |
| {copySuccess ? 'Copied!' : 'Copy to Clipboard'} | |
| </Button> | |
| <Button | |
| onClick={downloadAsFile} | |
| variant="outline" | |
| className="border-2 border-gray-300 hover:bg-gray-50" | |
| > | |
| <Download className="h-4 w-4 mr-2" /> | |
| Download | |
| </Button> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| ReactDOM.render(<ArcProjectStarter />, document.getElementById('root')); | |
| </script> | |
| </body> | |
| </html> |