import React, { useState, useRef, useEffect } from 'react';
import { ChevronLeft, ChevronRight, Wheat, Target, HeartHandshake, Image as ImageIcon, Maximize, Minimize, Printer, Download, Loader2 } from 'lucide-react';
const slides = [
{
title: "アメリカン・クラフトビールの夜明け",
titleFontSize: '5.2cqw',
subtitle: "「反逆」から始まった歴史",
icon: <Wheat style={{ width: '5cqw', height: '5cqw' }} className="text-amber-500" />,
image: "https://cowboycraftjapan.com/cdn/shop/articles/Copy_of_Bacon_and_BeerClassic.png?v=1574982900&width=1200",
imageAlt: "ガレージでビール造りをしている風景のイラスト",
items: [
{
heading: "画一化された市場への反骨精神",
details: [
"1970年代以前、市場は「薄くて軽い大量生産ビール」が主流。",
"退屈な状況に対するカウンターカルチャーとして誕生。"
]
},
{
heading: "歴史を動かした1978年の法改正",
details: [
"ホームブルーイング(家庭での自家醸造)の合法化が最大の転機。"
]
},
{
heading: "ガレージから生まれたDIY精神",
details: [
"独自の味を追求する愛好家が次々とプロの醸造家へ転身。",
"現在の多様なビール文化の原点に。"
]
}
]
},
{
title: "クラフトビールを名乗るための厳格な基準",
titleFontSize: '4.5cqw',
subtitle: "業界団体(BA)が定める「3つの定義」",
icon: <Target style={{ width: '5cqw', height: '5cqw' }} className="text-amber-500" />,
image: "https://cdn.brewersassociation.org/wp-content/uploads/2019/06/ba_legal-social.jpg",
imageAlt: "BA(アメリカのブルワーズアソシエーション)のロゴ",
items: [
{
heading: "Small(小規模であること)",
details: [
"年間生産量が約7億リットル以下。",
"メガブルワリーとは明確に一線を画す規模。"
]
},
{
heading: "Independent(独立していること)",
details: [
"外部の巨大資本からの出資・支配が「25%未満」。",
"自分たちの意志でビール造りをコントロールする証。"
]
},
{
heading: "Brewer(醸造業者であること)",
details: [
"政府から認可を受けてビールを製造していること。",
"※2018年に「伝統的製法」の縛りが撤廃され、より自由な醸造へ。"
]
}
]
},
{
title: "グラスに注がれるフィロソフィー",
titleFontSize: '5.2cqw',
subtitle: "ビールに込められた「インディーズ精神」",
icon: <HeartHandshake style={{ width: '5cqw', height: '5cqw' }} className="text-amber-500" />,
image: "https://vhx.imgix.net/brewdogbelieve/assets/47f38d11-010d-47f1-ad09-9f53d5aa6fb8.jpg?auto=format%2Ccompress&fit=crop&h=360&q=70&w=640",
imageAlt: "パンクIPAの写真",
items: [
{
heading: "巨大資本への抵抗と「独立」の誇り",
details: [
"買収攻勢に屈せず、造り手の情熱とリスクを尊ぶ。"
]
},
{
heading: "「伝統」よりも「イノベーション(革新)」",
details: [
"古いルールに縛られず、自由な発想で新しい味を追求。"
]
},
{
heading: "コスト削減へのアンチテーゼ",
details: [
"豊かな風味と最高品質のためなら、高価な原料や手間を惜しまない。"
]
},
{
heading: "ローカリズム(地域社会との共生)",
details: [
"地域の人々が集うハブ(タップルーム)であることを重視。"
]
}
]
}
];
// --- スライド1枚分を描画する共通コンポーネント ---
const SlideContent = ({ slide, pageIndex, totalSlides, isExport = false }) => (
<div
className={`slide-container w-full aspect-video bg-stone-900 relative flex flex-col overflow-hidden ${isExport ? '' : 'max-w-5xl rounded-xl shadow-2xl'}`}
style={{ containerType: 'inline-size' }}
>
{/* 背景画像とオーバーレイ */}
<img
src={slide.image}
alt="背景イメージ"
crossOrigin="anonymous"
className="absolute inset-0 w-full h-full object-cover filter brightness-[0.4] sepia-[0.2]"
/>
<div className="absolute inset-0 bg-gradient-to-r from-stone-900/95 via-stone-900/80 to-stone-900/30 z-0"></div>
{/* 背景テクスチャ */}
<div
className="absolute inset-0 opacity-20 mix-blend-overlay pointer-events-none z-0"
style={{ backgroundImage: "url('https://www.transparenttextures.com/patterns/cream-paper.png')" }}
></div>
{/* トップのアクセントライン */}
<div className="absolute top-0 left-0 right-0 bg-amber-500 z-20" style={{ height: '0.8cqw' }}></div>
{/* コンテンツエリア */}
<div className="relative z-10 w-full h-full flex flex-col" style={{ padding: '3cqw 4cqw 2cqw 4cqw' }}>
{/* ヘッダー部分 */}
<div className="border-b-2 border-amber-500/30" style={{ paddingBottom: '1.5cqw', marginBottom: '1.5cqw' }}>
<div className="flex items-center" style={{ gap: '1.2cqw', marginBottom: '0.5cqw' }}>
{slide.icon}
<p className="text-amber-500 font-bold tracking-widest uppercase drop-shadow" style={{ fontSize: '2.4cqw' }}>
{slide.subtitle}
</p>
</div>
<h1 className="font-serif font-bold text-stone-50 leading-tight drop-shadow-lg whitespace-nowrap" style={{ fontSize: slide.titleFontSize || '5.2cqw' }}>
{slide.title}
</h1>
</div>
{/* 箇条書きリスト部分 */}
<div className="flex-1 flex flex-col justify-center" style={{ gap: '1.5cqw' }}>
{slide.items.map((item, idx) => (
<div key={idx} className="flex items-start" style={{ gap: '1.5cqw' }}>
{/* 番号アイコン */}
<div
className="flex-shrink-0 rounded-full bg-amber-600 text-stone-50 flex items-center justify-center font-bold font-serif shadow-md"
style={{ width: '3.6cqw', height: '3.6cqw', fontSize: '2cqw', marginTop: '0.2cqw' }}
>
{idx + 1}
</div>
{/* テキスト内容 */}
<div className="w-full max-w-[90%]">
<h3 className="font-bold text-stone-100 font-serif drop-shadow" style={{ fontSize: '3cqw', marginBottom: '0.6cqw' }}>
{item.heading}
</h3>
<ul style={{ gap: '0.4cqw', display: 'flex', flexDirection: 'column' }}>
{item.details.map((detail, dIdx) => (
<li key={dIdx} className="text-stone-300 flex items-start drop-shadow-sm" style={{ fontSize: '2.4cqw', lineHeight: '1.35' }}>
<span className="text-amber-500" style={{ fontSize: '2cqw', marginRight: '0.8cqw', marginTop: '0.3cqw' }}>◆</span>
<span>{detail}</span>
</li>
))}
</ul>
</div>
</div>
))}
</div>
{/* フッターエリア(ページ番号と画像ラベル) */}
<div className="flex justify-between items-end mt-auto">
<div className="font-serif font-bold text-stone-400" style={{ fontSize: '2.4cqw' }}>
{pageIndex + 1} / {totalSlides}
</div>
<div className="flex items-center gap-1.5 bg-stone-900/60 px-3 py-1.5 rounded backdrop-blur-sm border border-stone-700/50">
<ImageIcon className="text-amber-400" style={{ width: '2.4cqw', height: '2.4cqw' }} />
<span className="text-stone-300" style={{ fontSize: '1.8cqw' }}>背景: {slide.imageAlt}</span>
</div>
</div>
</div>
</div>
);
export default function App() {
const [currentSlide, setCurrentSlide] = useState(0);
const [isFullscreen, setIsFullscreen] = useState(false);
const [isExporting, setIsExporting] = useState(false);
const containerRef = useRef(null);
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
}, []);
const toggleFullscreen = async () => {
if (!document.fullscreenElement) {
try {
if (containerRef.current?.requestFullscreen) {
await containerRef.current.requestFullscreen();
} else {
console.warn("Fullscreen API is not supported or permitted in this environment.");
}
} catch (err) {
console.warn(`Fullscreen API Error: ${err.message}.`);
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
};
// PDF直接ダウンロード機能 (html2pdf.jsを使用)
const handleExportPDF = () => {
setIsExporting(true);
const generatePDF = () => {
const element = document.getElementById('pdf-export-area');
const opt = {
margin: 0,
filename: 'American_Craft_Beer_Slides.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, letterRendering: true, backgroundColor: '#1c1917' },
jsPDF: { unit: 'px', format: [1920, 1080], orientation: 'landscape' },
pagebreak: { mode: 'avoid-all' }
};
window.html2pdf().set(opt).from(element).save().then(() => {
setIsExporting(false);
}).catch(err => {
console.error("PDFエクスポートエラー:", err);
setIsExporting(false);
// フォールバックとしてブラウザの印刷を呼び出し
window.print();
});
};
// CDNからライブラリを動的に読み込む
if (window.html2pdf) {
generatePDF();
} else {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js';
script.onload = generatePDF;
script.onerror = () => {
console.warn("PDFライブラリの読み込みがブロックされました。ブラウザの印刷機能を使用します。");
setIsExporting(false);
window.print();
};
document.body.appendChild(script);
}
};
const nextSlide = () => setCurrentSlide((prev) => (prev + 1) % slides.length);
const prevSlide = () => setCurrentSlide((prev) => (prev - 1 + slides.length) % slides.length);
return (
<>
{/* 印刷用の特殊なCSS(背景色や画像の強制印刷、レイアウトの固定化) */}
<style>
{`
@media print {
@page { size: landscape; margin: 0; }
body { margin: 0; -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; background-color: white !important; }
.print-page { page-break-after: always; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.slide-container { width: 100vw !important; aspect-ratio: 16/9 !important; max-width: none !important; border-radius: 0 !important; box-shadow: none !important; }
}
`}
</style>
<div ref={containerRef} className="min-h-screen bg-stone-900 print:bg-white flex flex-col items-center justify-center p-4 sm:p-8 font-sans print:p-0 print:m-0">
{/* --- 通常表示エリア (印刷時は非表示) --- */}
<div className="print:hidden w-full flex flex-col items-center relative z-10">
<SlideContent
slide={slides[currentSlide]}
pageIndex={currentSlide}
totalSlides={slides.length}
/>
{/* スライド操作用のコントローラー */}
<div className="flex items-center justify-center mt-8 relative w-full max-w-5xl">
<div className="flex items-center gap-6">
<button
onClick={prevSlide}
className="p-3 rounded-full bg-stone-800 text-stone-300 hover:bg-amber-700 hover:text-white transition-colors"
aria-label="前のスライド"
>
<ChevronLeft className="w-6 h-6" />
</button>
<div className="flex gap-3">
{slides.map((_, idx) => (
<button
key={idx}
onClick={() => setCurrentSlide(idx)}
className={`w-3 h-3 rounded-full transition-colors ${
currentSlide === idx ? 'bg-amber-600 ring-2 ring-amber-600/50 ring-offset-2 ring-offset-stone-900' : 'bg-stone-600 hover:bg-stone-400'
}`}
aria-label={`スライド ${idx + 1} へ`}
/>
))}
</div>
<button
onClick={nextSlide}
className="p-3 rounded-full bg-stone-800 text-stone-300 hover:bg-amber-700 hover:text-white transition-colors"
aria-label="次のスライド"
>
<ChevronRight className="w-6 h-6" />
</button>
</div>
{/* 右側のユーティリティボタン群 */}
<div className="absolute right-0 hidden sm:flex items-center gap-3">
{/* PDF直接ダウンロード */}
<button
onClick={handleExportPDF}
disabled={isExporting}
className={`p-3 rounded-full text-stone-300 transition-colors ${isExporting ? 'bg-amber-700 opacity-80 cursor-wait' : 'bg-stone-800 hover:bg-amber-700 hover:text-white'}`}
aria-label="PDFとしてダウンロード"
title="全スライドをPDFファイルとしてダウンロード"
>
{isExporting ? <Loader2 className="w-6 h-6 animate-spin" /> : <Download className="w-6 h-6" />}
</button>
{/* ブラウザ印刷機能 */}
<button
onClick={() => window.print()}
className="p-3 rounded-full bg-stone-800 text-stone-300 hover:bg-amber-700 hover:text-white transition-colors"
aria-label="印刷プレビュー"
title="ブラウザの印刷・PDF保存機能"
>
<Printer className="w-6 h-6" />
</button>
{/* 全画面表示の切り替え */}
<button
onClick={toggleFullscreen}
className="p-3 rounded-full bg-stone-800 text-stone-300 hover:bg-amber-700 hover:text-white transition-colors"
aria-label="全画面表示の切り替え"
title="全画面表示の切り替え"
>
{isFullscreen ? <Minimize className="w-6 h-6" /> : <Maximize className="w-6 h-6" />}
</button>
</div>
</div>
</div>
{/* --- エクスポート(PDFダウンロード)用エリア (画面外に配置) --- */}
<div style={{ position: 'fixed', top: '200vh', left: '-9999px', pointerEvents: 'none', zIndex: -100 }}>
<div id="pdf-export-area" style={{ width: '1920px', backgroundColor: '#1c1917', display: 'flex', flexDirection: 'column' }}>
{slides.map((slide, idx) => (
<div key={idx} style={{ width: '1920px', height: '1080px', position: 'relative', overflow: 'hidden' }}>
<SlideContent
slide={slide}
pageIndex={idx}
totalSlides={slides.length}
isExport={true}
/>
</div>
))}
</div>
</div>
{/* --- 印刷(ブラウザ機能)用エリア (通常時は非表示) --- */}
<div className="hidden print:block w-full">
{slides.map((slide, idx) => (
<div key={idx} className="print-page">
<SlideContent
slide={slide}
pageIndex={idx}
totalSlides={slides.length}
isExport={true}
/>
</div>
))}
</div>
</div>
</>
);
}