
【大バズリ】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) => {returnnew 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); });};exportdefault functionApp() {const [isDragging, setIsDragging] = useState(false);const [file, setFile] = useState(null);const [status, setStatus] = useState('idle');// idle, processing, zipping, complete, errorconst [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 =awaitnew 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` });awaitnew 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;awaitnew 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';elseif (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;awaitnew 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">Optimizedfor 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-pointergroup 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">PNGImages (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"> <selectvalue={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" > <optionvalue={0.5}>0.5s</option> <optionvalue={1.0}>1.0s</option> <optionvalue={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"> Developedby <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-110s infinite ease-in-out; } .animate-aurora-2 { animation: aurora-212s infinite ease-in-out; } .animate-aurora-3 { animation: aurora-38s 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-up0.8s cubic-bezier(0.2,0.8,0.2,1) forwards; } `}</style> </div> );}こんな感じでプレビューされたら成功です。
後は社員やお友達にリンクを共有すればOK!

【メンバーシップ限定】バイブコーディングで作ってみよう!
これはバイブコーディングで30分くらいで完成したのですが、
具体的にどんな指示をGeminiに投げたのか、解説していきます。
超有益なプロンプトテクニックも紹介しておりますので、
気になる方はnoteメンバーシップへの加入をぜひ。
メンバーシップはこちら👇️
さて、それでは第一声からいきますよ!
ここから先は
25,389字 / 12画像
メンバーシップ¥980/月
Google Workspace × Gemini 活用の研究コミュニティ「Majincraft(…
PayPay決済で追加チャンス!12/28まで
この記事が気に入ったらチップで応援してみませんか?

