【大バズリ】NotebookLMスライドをパワポやGIF画像に即変換するCanvas。ん…Canvas!?【コード無料配布】
こんにちは、まじんです。
昨日Xで投稿したこちらのポストがバズってまして。
これにNotebookLMスライドを渡してみて。https://t.co/yxtJNMViWQ
— まじん (@Majin_AppSheet) December 12, 2025
いや……投稿から1日経ってないのに
まじん式v4 のいいね/インプの半数超えてるんだが…。
しかもこれ、
Geminiと30分くらいお喋りしてたら完成しました。
今回は、このアプリのソースコードを無料公開したいと思います!
また、どのようにGeminiと対話したのか?
そのバイブコーディングのテクニックについては、
メンバーシップ限定エリアに記載しますね、さすがに。
【無料公開】自分のGeminiで再現しよう!
Gemini(思考モード)でCanvasをオンにして、
以下のプロンプトを送信してみてください。無料GeminiでもOK。
以下のコードをCanvasにそのままプレビューしてください。
---
[ソースコード]
ソースコード(Xで公開したこちら)
import React, { useState, useEffect, useRef } from 'react';
import { Upload, Download, Loader2, CheckCircle2, X, Film, FileArchive, ScrollText, Settings2, Presentation, Sparkles, FileText, Zap, RefreshCw } from 'lucide-react';
// 外部ライブラリの読み込みヘルパー
const loadScript = (src) => {
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) {
resolve();
return;
}
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.body.appendChild(script);
});
};
export default function App() {
const [isDragging, setIsDragging] = useState(false);
const [file, setFile] = useState(null);
const [status, setStatus] = useState('idle'); // idle, processing, zipping, complete, error
const [progress, setProgress] = useState({ current: 0, total: 0, message: '' });
const [generatedImages, setGeneratedImages] = useState([]);
const [zipBlob, setZipBlob] = useState(null);
const [errorMsg, setErrorMsg] = useState('');
const [librariesLoaded, setLibrariesLoaded] = useState(false);
const [gifStatus, setGifStatus] = useState('idle');
const [longImageStatus, setLongImageStatus] = useState('idle');
const [pptxStatus, setPptxStatus] = useState('idle');
const [gifInterval, setGifInterval] = useState(1.0); // 秒単位
const fileInputRef = useRef(null);
const cancelProcessingRef = useRef(false);
useEffect(() => {
const loadLibs = async () => {
try {
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js');
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js');
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js');
await loadScript('https://cdn.jsdelivr.net/npm/pptxgenjs@3.12.0/dist/pptxgen.bundle.js');
if (window.pdfjsLib) {
window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
}
setLibrariesLoaded(true);
} catch (err) {
setErrorMsg('ライブラリの読み込みに失敗しました。リロードしてください。');
}
};
loadLibs();
}, []);
const handleDragOver = (e) => { e.preventDefault(); setIsDragging(true); };
const handleDragLeave = (e) => { e.preventDefault(); setIsDragging(false); };
const handleDrop = (e) => {
e.preventDefault();
setIsDragging(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
const droppedFile = e.dataTransfer.files[0];
droppedFile.type === 'application/pdf' ? startProcessing(droppedFile) : setErrorMsg('PDFファイルのみ対応しています。');
}
};
const handleFileSelect = (e) => {
if (e.target.files && e.target.files[0]) startProcessing(e.target.files[0]);
};
const startProcessing = async (selectedFile) => {
if (!librariesLoaded) return;
setFile(selectedFile);
setStatus('processing');
setGeneratedImages([]);
setZipBlob(null);
setErrorMsg('');
setGifStatus('idle');
setLongImageStatus('idle');
setPptxStatus('idle');
setProgress({ current: 0, total: 0, message: 'Analyzing Structure...' });
cancelProcessingRef.current = false;
try {
const arrayBuffer = await selectedFile.arrayBuffer();
const pdf = await window.pdfjsLib.getDocument(arrayBuffer).promise;
const totalPages = pdf.numPages;
setProgress({ current: 0, total: totalPages, message: `Rendering Slides...` });
const images = [];
const zip = new window.JSZip();
const folder = zip.folder("images");
for (let i = 1; i <= totalPages; i++) {
if (cancelProcessingRef.current) break;
const page = await pdf.getPage(i);
const scale = 2.0;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context, viewport }).promise;
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
const dataUrl = canvas.toDataURL('image/png');
images.push({ id: i, dataUrl, name: `slide_${String(i).padStart(2, '0')}.png`, width: viewport.width, height: viewport.height });
folder.file(`slide_${String(i).padStart(2, '0')}.png`, blob);
setProgress({ current: i, total: totalPages, message: `${Math.round((i / totalPages) * 100)}% Complete` });
await new Promise(r => setTimeout(r, 20));
}
if (cancelProcessingRef.current) { setStatus('idle'); setFile(null); return; }
setGeneratedImages(images);
setStatus('zipping');
setProgress(prev => ({ ...prev, message: 'Packaging Assets...' }));
const content = await zip.generateAsync({ type: "blob" });
setZipBlob(content);
setStatus('complete');
} catch (err) {
console.error(err);
setErrorMsg('処理中にエラーが発生しました。ファイルを確認してください。');
setStatus('error');
}
};
const downloadFile = (blob, filename) => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const handleDownloadZip = () => zipBlob && file && downloadFile(zipBlob, `${file.name.replace('.pdf', '')}_slides.zip`);
const generateGif = async () => {
if (!window.GIF || generatedImages.length === 0) return;
setGifStatus('processing');
try {
const workerResponse = await fetch('https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.worker.js');
const workerBlob = await workerResponse.blob();
const workerUrl = URL.createObjectURL(workerBlob);
const gif = new window.GIF({ workers: 2, quality: 10, workerScript: workerUrl, width: generatedImages[0].width, height: generatedImages[0].height });
for (const imgData of generatedImages) {
const img = new Image();
img.src = imgData.dataUrl;
await new Promise(resolve => { img.complete ? resolve() : img.onload = resolve; });
gif.addFrame(img, { delay: gifInterval * 1000 });
}
gif.on('finished', (blob) => {
downloadFile(blob, `${file.name.replace('.pdf', '')}_animation.gif`);
setGifStatus('complete');
URL.revokeObjectURL(workerUrl);
});
gif.render();
} catch (err) { setGifStatus('error'); }
};
const generatePptx = async () => {
if (!window.PptxGenJS || generatedImages.length === 0) return;
setPptxStatus('processing');
try {
const pptx = new window.PptxGenJS();
const firstImg = generatedImages[0];
const ratio = firstImg.width / firstImg.height;
if (Math.abs(ratio - 16/9) < 0.1) pptx.layout = 'LAYOUT_16x9';
else if (Math.abs(ratio - 4/3) < 0.1) pptx.layout = 'LAYOUT_4x3';
else { pptx.defineLayout({ name: 'CUSTOM', width: 10, height: 10 / ratio }); pptx.layout = 'CUSTOM'; }
for (const imgData of generatedImages) {
const slide = pptx.addSlide();
slide.addImage({ data: imgData.dataUrl, x: 0, y: 0, w: '100%', h: '100%' });
}
await pptx.writeFile({ fileName: `${file.name.replace('.pdf', '')}.pptx` });
setPptxStatus('complete');
} catch (err) { setPptxStatus('error'); }
};
const generateLongImage = async () => {
if (generatedImages.length === 0) return;
setLongImageStatus('processing');
try {
const maxWidth = Math.max(...generatedImages.map(img => img.width));
const totalHeight = generatedImages.reduce((sum, img) => sum + img.height, 0);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = maxWidth;
canvas.height = totalHeight;
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
let currentY = 0;
for (const imgData of generatedImages) {
const img = new Image();
img.src = imgData.dataUrl;
await new Promise(resolve => { img.complete ? resolve() : img.onload = resolve; });
const x = (maxWidth - imgData.width) / 2;
ctx.drawImage(img, x, currentY);
currentY += imgData.height;
}
canvas.toBlob((blob) => {
if (blob) {
downloadFile(blob, `${file.name.replace('.pdf', '')}_long.png`);
setLongImageStatus('complete');
} else setLongImageStatus('error');
}, 'image/png');
} catch (err) { setLongImageStatus('error'); }
};
const reset = () => {
setFile(null);
setStatus('idle');
setGifStatus('idle');
setLongImageStatus('idle');
setPptxStatus('idle');
setGeneratedImages([]);
setZipBlob(null);
setErrorMsg('');
setProgress({ current: 0, total: 0, message: '' });
if (fileInputRef.current) fileInputRef.current.value = '';
};
const cancel = () => { cancelProcessingRef.current = true; reset(); };
const progressPercent = progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0;
return (
<div className="relative min-h-screen bg-slate-950 text-slate-200 font-sans p-6 flex flex-col items-center justify-center overflow-hidden selection:bg-indigo-500/30 selection:text-indigo-100">
{/* Aurora Background Effects */}
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-[-10%] left-[-10%] w-[50%] h-[50%] bg-indigo-600/20 rounded-full blur-[100px] animate-aurora-1"></div>
<div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] bg-cyan-600/20 rounded-full blur-[100px] animate-aurora-2"></div>
<div className="absolute top-[40%] left-[40%] w-[30%] h-[30%] bg-fuchsia-600/10 rounded-full blur-[100px] animate-aurora-3"></div>
<div className="absolute inset-0 opacity-[0.03] bg-[url('https://www.transparenttextures.com/patterns/stardust.png')]"></div>
</div>
<div className="w-full max-w-5xl space-y-8 relative z-10">
{/* Header Section (Dynamic) */}
<div className={`text-center space-y-6 pt-4 transition-all duration-500 ${status === 'complete' ? 'mb-4' : ''}`}>
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-white/5 border border-white/10 shadow-[0_0_15px_rgba(255,255,255,0.05)] mb-2 animate-fade-in-up backdrop-blur-md">
<Sparkles className="w-3.5 h-3.5 text-cyan-300" />
<span className="text-[11px] uppercase tracking-[0.2em] font-bold text-cyan-300/90">Optimized for NotebookLM</span>
</div>
{status !== 'complete' && (
<>
<h1 className="text-4xl md:text-6xl font-extrabold tracking-tight text-white leading-tight drop-shadow-2xl animate-fade-in-up" style={{animationDelay: '0.1s'}}>
静止したスライドを、<br/>
<span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-300 via-indigo-300 to-fuchsia-300">自在なビジュアルへ。</span>
</h1>
<p className="text-slate-400 text-lg md:text-xl max-w-2xl mx-auto font-light leading-relaxed animate-fade-in-up" style={{animationDelay: '0.2s'}}>
PDFを瞬時に<span className="text-indigo-200 font-medium">高解像度で画像化</span>し、PowerPointやGIFに再構築。<br className="hidden md:block"/>
並べ替えも、シェアも、あなたの思いのままに。
</p>
</>
)}
</div>
{/* Glassmorphism Main Card Container */}
<div className={`transition-all duration-500 ${status === 'complete' ? 'max-w-4xl mx-auto' : ''}`}>
{/* Upload Area */}
{status === 'idle' && (
<div className="bg-white/5 backdrop-blur-2xl rounded-[2.5rem] shadow-[0_8px_32px_0_rgba(0,0,0,0.36)] border border-white/10 overflow-hidden relative">
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`
relative p-24 text-center cursor-pointer group transition-all duration-500
${isDragging ? 'bg-indigo-500/10' : 'hover:bg-white/5'}
`}
onClick={() => fileInputRef.current?.click()}
>
<input type="file" ref={fileInputRef} className="hidden" accept="application/pdf" onChange={handleFileSelect} />
<div className={`absolute inset-6 rounded-[1.5rem] border border-dashed transition-all duration-500 ${isDragging ? 'border-indigo-400/50 bg-indigo-500/5' : 'border-slate-600/50 group-hover:border-slate-500/80'}`} />
<div className="relative z-10 flex flex-col items-center gap-8">
<div className={`
w-24 h-24 rounded-3xl flex items-center justify-center shadow-2xl transition-all duration-500 border border-white/10
${isDragging ? 'bg-indigo-500 text-white scale-110 rotate-3 shadow-indigo-500/40' : 'bg-slate-800/50 text-cyan-300 group-hover:scale-105 group-hover:bg-slate-800/80'}
`}>
<FileText className="w-10 h-10" />
</div>
<div className="space-y-3">
<p className="text-2xl font-bold text-slate-200 group-hover:text-white transition-colors">
PDFファイルをここにドロップ
</p>
<p className="text-sm text-slate-500 group-hover:text-slate-400">
または <span className="text-cyan-300 underline decoration-cyan-900 underline-offset-4 group-hover:decoration-cyan-400 transition-all">ファイルを選択</span>
</p>
</div>
{!librariesLoaded && (
<div className="inline-flex items-center gap-2 px-4 py-2 bg-black/20 border border-white/5 text-slate-400 text-xs font-medium rounded-full mt-4 backdrop-blur-sm">
<Loader2 className="w-3 h-3 animate-spin text-cyan-400" /> System Initializing...
</div>
)}
</div>
</div>
</div>
)}
{/* Processing View */}
{(status === 'processing' || status === 'zipping') && (
<div className="bg-white/5 backdrop-blur-2xl rounded-[2.5rem] shadow-[0_8px_32px_0_rgba(0,0,0,0.36)] border border-white/10 overflow-hidden p-24 flex flex-col items-center justify-center min-h-[500px]">
<div className="relative w-48 h-48 mb-10">
<div className="absolute inset-0 bg-indigo-500/20 blur-3xl rounded-full animate-pulse"></div>
<svg className="w-full h-full rotate-[-90deg] relative z-10" viewBox="0 0 100 100">
<circle className="text-slate-800/50 stroke-current" strokeWidth="2" cx="50" cy="50" r="46" fill="transparent"></circle>
<circle
className="text-cyan-400 stroke-current transition-all duration-300 ease-out drop-shadow-[0_0_8px_rgba(34,211,238,0.8)]"
strokeWidth="2" strokeLinecap="round" cx="50" cy="50" r="46" fill="transparent"
strokeDasharray={`${289.02 * (progressPercent / 100)} 289.02`}
></circle>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center z-20">
<span className="text-5xl font-black text-white tracking-tighter drop-shadow-lg">{progressPercent}%</span>
</div>
</div>
<h3 className="text-2xl font-bold text-white mb-2 tracking-wide animate-pulse">
{status === 'zipping' ? 'PACKAGING...' : 'CONVERTING SLIDES...'}
</h3>
<p className="text-indigo-200/60 font-mono text-sm mb-10">{progress.message}</p>
<button onClick={cancel} className="text-slate-500 hover:text-white text-xs tracking-widest font-bold px-8 py-3 rounded-full hover:bg-white/10 border border-transparent hover:border-white/10 transition-all backdrop-blur-md">
CANCEL
</button>
</div>
)}
{/* Completion View - New Layout */}
{status === 'complete' && (
<div className="flex flex-col items-center w-full animate-fade-in-up">
{/* Compact Header Info */}
<div className="text-center mb-8 w-full">
<div className="inline-flex items-center gap-2 px-4 py-2 bg-emerald-500/10 border border-emerald-500/20 rounded-full text-emerald-400 mb-4 backdrop-blur-md">
<CheckCircle2 className="w-4 h-4" />
<span className="text-sm font-bold tracking-wide">Ready to Download</span>
</div>
<h2 className="text-2xl md:text-3xl font-bold text-white mb-2 drop-shadow-lg line-clamp-1 px-4">{file?.name}</h2>
<div className="flex items-center justify-center gap-4 text-sm text-slate-400 font-mono">
<span className="bg-white/5 px-3 py-1 rounded-md border border-white/5">{generatedImages.length} Slides</span>
<span className="bg-white/5 px-3 py-1 rounded-md border border-white/5">{(zipBlob?.size / 1024 / 1024).toFixed(1)} MB</span>
</div>
</div>
{/* Action Grid (Bento Style) */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full">
{/* PNG (ZIP) - Priority */}
<button onClick={handleDownloadZip} className="group relative col-span-1 md:col-span-2 p-6 bg-slate-800/40 backdrop-blur-xl border border-white/10 rounded-[1.5rem] hover:bg-slate-700/60 hover:border-cyan-500/30 transition-all duration-300 text-left overflow-hidden shadow-lg hover:shadow-cyan-500/20">
<div className="absolute top-0 right-0 p-32 bg-cyan-500/10 rounded-full blur-3xl group-hover:bg-cyan-400/20 transition-all duration-500 translate-x-1/2 -translate-y-1/2"></div>
<div className="flex items-start justify-between relative z-10">
<div className="space-y-1">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 bg-cyan-500/20 rounded-lg text-cyan-300 border border-cyan-500/20 shadow-[0_0_15px_rgba(6,182,212,0.3)]">
<FileArchive className="w-5 h-5" />
</div>
<span className="text-[10px] font-bold text-cyan-300/80 uppercase tracking-wider border border-cyan-500/20 px-2 py-0.5 rounded-full bg-cyan-500/5">Recommended</span>
</div>
<h4 className="text-2xl font-bold text-white group-hover:text-cyan-50 transition-colors">PNG Images (ZIP)</h4>
<p className="text-sm text-slate-400 group-hover:text-slate-300">全てのページを個別の高解像度画像として一括保存</p>
</div>
<div className="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center group-hover:bg-cyan-500 group-hover:text-white transition-all shadow-inner border border-white/5">
<Download className="w-6 h-6" />
</div>
</div>
</button>
{/* PPTX */}
<button onClick={generatePptx} disabled={pptxStatus === 'processing'} className="group relative p-6 bg-slate-800/40 backdrop-blur-xl border border-white/10 rounded-[1.5rem] hover:bg-slate-700/60 hover:border-orange-500/30 transition-all duration-300 text-left overflow-hidden shadow-lg hover:shadow-orange-500/20 disabled:opacity-50">
<div className="absolute bottom-0 left-0 p-20 bg-orange-500/10 rounded-full blur-3xl group-hover:bg-orange-400/20 transition-all duration-500 -translate-x-1/3 translate-y-1/3"></div>
<div className="flex flex-col h-full justify-between relative z-10">
<div className="flex justify-between items-start mb-4">
<div className="p-2 bg-orange-500/20 rounded-lg text-orange-300 border border-orange-500/20 shadow-[0_0_15px_rgba(249,115,22,0.3)]">
{pptxStatus === 'processing' ? <Loader2 className="w-5 h-5 animate-spin" /> : <Presentation className="w-5 h-5" />}
</div>
{pptxStatus === 'complete' ?
<div className="bg-emerald-500/20 text-emerald-400 p-1.5 rounded-full border border-emerald-500/20"><CheckCircle2 className="w-4 h-4" /></div>
: <div className="bg-white/5 p-1.5 rounded-full group-hover:bg-orange-500/20 group-hover:text-orange-300 transition-colors"><Download className="w-4 h-4 text-slate-500" /></div>
}
</div>
<div>
<h4 className="text-lg font-bold text-white mb-0.5 group-hover:text-orange-50 transition-colors">PowerPoint</h4>
<p className="text-[11px] text-slate-400">画像貼り付け済みスライド (PPTX)</p>
</div>
</div>
</button>
{/* GIF & Long PNG Cell */}
<div className="flex flex-col gap-4">
{/* GIF */}
<div className="group relative p-4 bg-slate-800/40 backdrop-blur-xl border border-white/10 rounded-[1.5rem] hover:bg-slate-700/60 hover:border-fuchsia-500/30 transition-all duration-300 overflow-hidden shadow-lg hover:shadow-fuchsia-500/20 flex items-center justify-between">
<div className="flex items-center gap-3 relative z-10">
<div className="p-2 bg-fuchsia-500/20 rounded-lg text-fuchsia-300 border border-fuchsia-500/20 shadow-[0_0_15px_rgba(217,70,239,0.3)]">
{gifStatus === 'processing' ? <Loader2 className="w-4 h-4 animate-spin" /> : <Film className="w-4 h-4" />}
</div>
<div>
<h4 className="text-sm font-bold text-white group-hover:text-fuchsia-50 transition-colors">GIF Animation</h4>
<div className="flex items-center gap-2 mt-0.5">
<select
value={gifInterval}
onChange={(e) => setGifInterval(Number(e.target.value))}
disabled={gifStatus === 'processing'}
className="text-[10px] bg-black/30 border border-white/10 rounded px-1.5 py-0.5 text-slate-300 focus:outline-none hover:bg-black/50 cursor-pointer"
>
<option value={0.5}>0.5s</option>
<option value={1.0}>1.0s</option>
<option value={2.0}>2.0s</option>
</select>
</div>
</div>
</div>
<button onClick={generateGif} disabled={gifStatus === 'processing'} className="relative z-10 text-[10px] font-bold bg-white/5 text-slate-300 border border-white/5 px-4 py-2 rounded-full hover:bg-fuchsia-500 hover:text-white hover:border-fuchsia-400 transition-all shadow-sm">
{gifStatus === 'complete' ? 'RETRY' : 'CREATE'}
</button>
</div>
{/* Long PNG */}
<button onClick={generateLongImage} disabled={longImageStatus === 'processing'} className="group relative p-4 bg-slate-800/40 backdrop-blur-xl border border-white/10 rounded-[1.5rem] hover:bg-slate-700/60 hover:border-blue-500/30 transition-all duration-300 overflow-hidden shadow-lg hover:shadow-blue-500/20 flex items-center justify-between text-left disabled:opacity-50 flex-1">
<div className="flex items-center gap-3 relative z-10">
<div className="p-2 bg-blue-500/20 rounded-lg text-blue-300 border border-blue-500/20 shadow-[0_0_15px_rgba(59,130,246,0.3)]">
{longImageStatus === 'processing' ? <Loader2 className="w-4 h-4 animate-spin" /> : <ScrollText className="w-4 h-4" />}
</div>
<div>
<h4 className="text-sm font-bold text-white group-hover:text-blue-50 transition-colors">Long PNG</h4>
<p className="text-[10px] text-slate-400">縦読みスクロール</p>
</div>
</div>
<div className="relative z-10">
{longImageStatus === 'complete' ?
<div className="bg-emerald-500/20 text-emerald-400 p-1 rounded-full"><CheckCircle2 className="w-4 h-4" /></div>
: <div className="bg-white/5 p-1.5 rounded-full group-hover:bg-blue-500/20 group-hover:text-blue-300 transition-colors"><Download className="w-4 h-4 text-slate-500" /></div>
}
</div>
</button>
</div>
</div>
<button
onClick={reset}
className="mt-8 text-slate-500 hover:text-white flex items-center gap-2 text-xs font-bold tracking-widest uppercase hover:underline decoration-slate-600 underline-offset-4 transition-colors relative z-10"
>
<RefreshCw className="w-3.5 h-3.5" /> Start Over
</button>
</div>
)}
{/* Error View */}
{status === 'error' && (
<div className="bg-white/5 backdrop-blur-2xl rounded-[2.5rem] shadow-[0_8px_32px_0_rgba(0,0,0,0.36)] border border-white/10 overflow-hidden p-20 text-center space-y-6">
<div className="w-20 h-20 bg-red-900/20 border border-red-500/20 text-red-500 rounded-3xl flex items-center justify-center mx-auto mb-4 shadow-[0_0_30px_rgba(239,68,68,0.1)] backdrop-blur-md">
<X className="w-10 h-10" />
</div>
<h3 className="text-xl font-bold text-slate-200">System Error</h3>
<p className="text-slate-500 max-w-md mx-auto">{errorMsg}</p>
<button onClick={reset} className="px-8 py-3 bg-white/10 text-white border border-white/10 rounded-xl hover:bg-white/20 transition-all font-medium shadow-lg backdrop-blur-md">
RETRY
</button>
</div>
)}
</div>
{/* Footer info */}
<div className="flex flex-col items-center justify-center space-y-3 pt-8 pb-4 opacity-40 hover:opacity-100 transition-opacity">
<div className="flex items-center gap-3 text-[10px] font-bold text-slate-500 tracking-[0.2em] uppercase">
<div className="flex items-center gap-1">
<Zap className="w-3 h-3" />
<span>Client-Side Processing</span>
</div>
<span className="w-1 h-1 rounded-full bg-slate-600"></span>
<span>No Uploads</span>
</div>
<p className="text-xs font-medium text-slate-600">
Developed by <span className="text-cyan-600/80">majin</span>
</p>
</div>
</div>
{/* Global Styles for Animations & Aurora */}
<style>{`
@keyframes aurora-1 {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(20px, -20px) scale(1.1); }
}
@keyframes aurora-2 {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(-20px, 10px) scale(1.2); }
}
@keyframes aurora-3 {
0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.3; }
50% { transform: translate(10px, 10px) scale(0.9); opacity: 0.6; }
}
.animate-aurora-1 { animation: aurora-1 10s infinite ease-in-out; }
.animate-aurora-2 { animation: aurora-2 12s infinite ease-in-out; }
.animate-aurora-3 { animation: aurora-3 8s infinite ease-in-out; }
@keyframes fade-in-up {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in-up {
animation: fade-in-up 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}
`}</style>
</div>
);
}こんな感じでプレビューされたら成功です。
後は社員やお友達にリンクを共有すればOK!
【メンバーシップ限定】バイブコーディングで作ってみよう!
これはバイブコーディングで30分くらいで完成したのですが、
具体的にどんな指示をGeminiに投げたのか、解説していきます。
超有益なプロンプトテクニックも紹介しておりますので、
気になる方はnoteメンバーシップへの加入をぜひ。
メンバーシップはこちら👇️
さて、それでは第一声からいきますよ!
ここから先は
25,389字
/
12画像
メンバーシップ
¥ 980 /月
この記事が気に入ったらチップで応援してみませんか?

