dehe / index.html
Ezmary's picture
Update index.html
fdd5912 verified
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>سفر در زمان (نسخه پیشرفته)</title>
<!-- 1. لود کردن Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 2. لود کردن فونت‌ها (وزیر برای فارسی) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&family=Permanent+Marker&family=Vazirmatn:wght@300;400;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Vazirmatn', sans-serif; background-color: black; }
.font-caveat { font-family: 'Caveat', cursive; }
.font-permanent-marker { font-family: 'Permanent Marker', cursive; }
#root:empty {
display: flex; align-items: center; justify-content: center;
min-height: 100vh; color: white; font-size: 1.5rem;
}
</style>
</head>
<body>
<div id="root">در حال بارگذاری ماشین زمان...</div>
<!-- 3. لود کردن کتابخانه‌های لازم -->
<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/framer-motion@10/dist/framer-motion.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel" data-type="module">
// =======================================================
// شروع کد اپلیکیشن یکپارچه
// =======================================================
const { useState, useEffect, useRef, StrictMode } = React;
const { motion, AnimatePresence, useMotionValue, useSpring, useTransform, useVelocity, useAnimationControls } = Motion;
// --- بخش ۱: منطق اصلی و ارتباط با API ---
const API_BASE_URL = "https://ginigen-nano-banana-Pro.hf.space/gradio_api/";
function dataURLtoFile(dataurl, filename) {
let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while(n--) u8arr[n] = bstr.charCodeAt(n);
return new File([u8arr], filename, {type:mime});
}
async function generateDecadeImage(uploadedImage, prompt) {
const imageFile = dataURLtoFile(uploadedImage, 'upload.png');
const formData = new FormData();
formData.append('files', imageFile);
const uploadResponse = await fetch(`${API_BASE_URL}upload`, { method: 'POST', body: formData });
if (!uploadResponse.ok) throw new Error(`آپلود ناموفق بود: ${uploadResponse.statusText}`);
const [filePath] = await uploadResponse.json();
const sessionHash = Math.random().toString(36).substring(7);
const payload = {
data: [prompt, { path: filePath, url: `${API_BASE_URL}file=${filePath}`, orig_name: "upload.png", meta: { _type: "gradio.FileData" } }, null],
event_data: null, fn_index: 0, session_hash: sessionHash
};
const joinResponse = await fetch(`${API_BASE_URL}queue/join`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
});
if (!joinResponse.ok) throw new Error(`ارسال درخواست ناموفق بود: ${joinResponse.statusText}`);
return new Promise((resolve, reject) => {
const eventSource = new EventSource(`${API_BASE_URL}queue/data?session_hash=${sessionHash}`);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.msg === 'process_completed') {
eventSource.close();
if (data.success && data.output?.data?.[0]?.url) {
resolve(data.output.data[0].url);
} else {
reject(new Error(data.output?.error || "خطای پردازش نامشخص."));
}
}
};
eventSource.onerror = (err) => {
eventSource.close();
reject(new Error("ارتباط با سرور برقرار نشد."));
};
});
}
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(img);
img.onerror = (err) => reject(new Error(`بارگذاری تصویر ناموفق بود`));
img.src = src;
});
}
async function createAlbumPage(imageData) {
const canvas = document.createElement('canvas');
const canvasWidth = 2480;
const canvasHeight = 3508;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Could not get 2D canvas context');
ctx.fillStyle = '#fdf5e6';
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
ctx.font = `bold 120px 'Caveat', cursive`;
ctx.fillText('سفر در زمان', canvasWidth / 2, 180);
const decades = Object.keys(imageData);
const loadedImages = await Promise.all(Object.values(imageData).map(url => loadImage(url)));
const imagesWithDecades = decades.map((decade, index) => ({ decade, img: loadedImages[index] }));
const grid = { cols: 2, rows: 3, padding: 100 };
const contentTopMargin = 300;
const contentHeight = canvasHeight - contentTopMargin;
const cellWidth = (canvasWidth - grid.padding * (grid.cols + 1)) / grid.cols;
const cellHeight = (contentHeight - grid.padding * (grid.rows + 1)) / grid.rows;
let polaroidWidth = cellWidth * 0.9;
let polaroidHeight = polaroidWidth * 1.2;
if (polaroidHeight > cellHeight * 0.9) {
polaroidHeight = cellHeight * 0.9;
polaroidWidth = polaroidHeight / 1.2;
}
const imageContainerWidth = polaroidWidth * 0.9;
const imageContainerHeight = imageContainerWidth;
[...imagesWithDecades].reverse().forEach(({ decade, img }, reversedIndex) => {
const index = imagesWithDecades.length - 1 - reversedIndex;
const row = Math.floor(index / grid.cols);
const col = index % grid.cols;
const x = grid.padding * (col + 1) + cellWidth * col + (cellWidth - polaroidWidth) / 2;
const y = contentTopMargin + grid.padding * (row + 1) + cellHeight * row + (cellHeight - polaroidHeight) / 2;
ctx.save();
ctx.translate(x + polaroidWidth / 2, y + polaroidHeight / 2);
ctx.rotate((Math.random() - 0.5) * 0.1);
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
ctx.shadowBlur = 35;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 10;
ctx.fillStyle = '#fff';
ctx.fillRect(-polaroidWidth / 2, -polaroidHeight / 2, polaroidWidth, polaroidHeight);
ctx.shadowColor = 'transparent';
const aspectRatio = img.naturalWidth / img.naturalHeight;
let drawWidth = imageContainerWidth;
let drawHeight = drawWidth / aspectRatio;
if (drawHeight > imageContainerHeight) {
drawHeight = imageContainerHeight;
drawWidth = drawHeight * aspectRatio;
}
const imageAreaTopMargin = (polaroidWidth - imageContainerWidth) / 2;
const imageContainerY = -polaroidHeight / 2 + imageAreaTopMargin;
const imgX = -drawWidth / 2;
const imgY = imageContainerY + (imageContainerHeight - drawHeight) / 2;
ctx.drawImage(img, imgX, imgY, drawWidth, drawHeight);
ctx.fillStyle = '#222';
ctx.font = `60px 'Permanent Marker', cursive`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const captionAreaTop = imageContainerY + imageContainerHeight;
const captionAreaBottom = polaroidHeight / 2;
const captionY = captionAreaTop + (captionAreaBottom - captionAreaTop) / 2;
ctx.fillText(decade.replace("دهه ", ""), 0, captionY);
ctx.restore();
});
return canvas.toDataURL('image/jpeg', 0.9);
}
function getPrimaryPrompt(decade) {
return `Reimagine the person in this photo in the style of the ${decade}. This includes clothing, hairstyle, photo quality, and the overall aesthetic of that decade. The output must be a photorealistic image showing the person clearly.`;
}
function getFallbackPrompt(decade) {
return `Create a photograph of the person in this image as if they were living in the ${decade}. The photograph should capture the distinct fashion, hairstyles, and overall atmosphere of that time period. Ensure the final image is a clear photograph that looks authentic to the era.`;
}
// --- بخش جدید: تابع ارتباط با والد برای دانلود ---
/**
* یک درخواست دانلود را از طریق postMessage به پنجره والد (iframe wrapper) ارسال می‌کند.
* اگر برنامه به صورت مستقل اجرا شود، به صورت مستقیم دانلود می‌کند.
* @param {string} url - آدرس تصویر برای دانلود (می‌تواند data: URL یا https باشد)
* @param {string} fallbackFilename - نام فایل برای دانلود در حالت مستقل
*/
function sendDownloadRequestToParent(url, fallbackFilename) {
// بررسی می‌کنیم که آیا اپلیکیشن داخل یک iframe اجرا می‌شود یا خیر
if (window.self !== window.top) {
console.log("در حال اجرا در iframe. ارسال پیام دانلود به والد...");
window.parent.postMessage({
type: 'DOWNLOAD_REQUEST',
url: url
}, '*'); // '*' به والد اجازه می‌دهد از هر مبدایی پیام را دریافت کند
} else {
// حالت جایگزین: اگر به صورت مستقل اجرا شود، دانلود مستقیم را انجام می‌دهد
console.warn("در حال اجرای مستقل. شروع دانلود مستقیم...");
const link = document.createElement('a');
link.href = url;
link.download = fallbackFilename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
// --- بخش ۲: کامپوننت‌های UI ---
const PolaroidCard = ({ imageUrl, caption, status, error, dragConstraintsRef, onShake, onDownload, isMobile }) => {
const [isDeveloped, setIsDeveloped] = useState(false);
const [isImageLoaded, setIsImageLoaded] = useState(false);
const lastShakeTime = useRef(0);
const lastVelocity = useRef({ x: 0, y: 0 });
useEffect(() => {
if (status === 'pending' || (status === 'done' && imageUrl)) {
setIsDeveloped(false); setIsImageLoaded(false);
}
}, [imageUrl, status]);
useEffect(() => {
if (isImageLoaded) {
const timer = setTimeout(() => setIsDeveloped(true), 200);
return () => clearTimeout(timer);
}
}, [isImageLoaded]);
const handleDrag = (event, info) => {
if (!onShake || isMobile) return;
const { x, y } = info.velocity;
const { x: prevX, y: prevY } = lastVelocity.current;
const now = Date.now();
if (Math.sqrt(x*x + y*y) > 1500 && (x*prevX + y*prevY < 0) && (now - lastShakeTime.current > 2000)) {
lastShakeTime.current = now; onShake(caption);
}
lastVelocity.current = { x, y };
};
const LoadingSpinner = () => <div className="flex items-center justify-center h-full"><svg className="animate-spin h-8 w-8 text-neutral-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg></div>;
const ErrorDisplay = () => <div className="flex items-center justify-center h-full"><svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg></div>;
const Placeholder = ({text}) => <div className="flex flex-col items-center justify-center h-full text-neutral-500 group-hover:text-neutral-300 transition-colors duration-300"><svg xmlns="http://www.w3.org/2000/svg" className="h-16 w-16 mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1}><path strokeLinecap="round" strokeLinejoin="round" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" /><path strokeLinecap="round" strokeLinejoin="round" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" /></svg><span className="font-permanent-marker text-xl">{text}</span></div>;
const cardInnerContent = (
<React.Fragment>
<div className="w-full bg-neutral-900 shadow-inner flex-grow relative overflow-hidden group">
{status === 'pending' && <LoadingSpinner />}
{status === 'error' && <ErrorDisplay />}
{status === 'done' && imageUrl && (
<React.Fragment>
<div className={`absolute top-2 left-2 z-20 flex flex-col gap-2 transition-opacity duration-300 ${!isMobile && "opacity-0 group-hover:opacity-100"}`}>
{onDownload && <button onClick={(e) => { e.stopPropagation(); onDownload(caption); }} className="p-2 bg-black/50 rounded-full text-white hover:bg-black/75 focus:outline-none focus:ring-2 focus:ring-white" aria-label={`دانلود ${caption}`}><svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg></button>}
{onShake && <button onClick={(e) => { e.stopPropagation(); onShake(caption); }} className="p-2 bg-black/50 rounded-full text-white hover:bg-black/75 focus:outline-none focus:ring-2 focus:ring-white" aria-label={`ساخت مجدد ${caption}`}><svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.899 2.186l-1.42.71a5.002 5.002 0 00-8.479-1.554H10a1 1 0 110 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm12 14a1 1 0 01-1-1v-2.101a7.002 7.002 0 01-11.899-2.186l1.42-.71a5.002 5.002 0 008.479 1.554H10a1 1 0 110-2h6a1 1 0 011 1v6a1 1 0 01-1 1z" clipRule="evenodd" /></svg></button>}
</div>
<div className={`absolute inset-0 z-10 bg-[#3a322c] transition-opacity duration-[3500ms] ease-out ${isDeveloped ? 'opacity-0' : 'opacity-100'}`} />
<img key={imageUrl} src={imageUrl} alt={caption} onLoad={() => setIsImageLoaded(true)} className={`w-full h-full object-cover transition-all duration-[4000ms] ease-in-out ${isDeveloped ? 'opacity-100 filter-none' : 'opacity-80 filter sepia(1) contrast(0.8) brightness(0.8)'}`} style={{ opacity: isImageLoaded ? undefined : 0 }} />
</React.Fragment>
)}
{status === 'done' && !imageUrl && <Placeholder text="برای شروع کلیک کنید"/>}
</div>
<div className="absolute bottom-4 left-4 right-4 text-center px-2">
<p className={`font-permanent-marker text-2xl truncate ${status === 'done' && imageUrl ? 'text-black' : 'text-neutral-800'}`}>{caption}</p>
</div>
</React.Fragment>
);
if (isMobile) {
return <div className="bg-neutral-100 !p-4 !pb-16 flex flex-col items-center justify-start aspect-[3/4] w-80 max-w-full rounded-md shadow-lg relative">{cardInnerContent}</div>;
}
const cardRef = useRef(null);
const controls = useAnimationControls();
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const springConfig = { stiffness: 100, damping: 20, mass: 0.5 };
const rotateX = useSpring(useTransform(mouseY, [-300, 300], [25, -25]), springConfig);
const rotateY = useSpring(useTransform(mouseX, [-300, 300], [-25, 25]), springConfig);
const handleMouseMove = (e) => {
if (cardRef.current && cardRef.current.style.transform.includes('translate3d')) return;
const { clientX, clientY } = e;
const { width, height, left, top } = cardRef.current?.getBoundingClientRect() ?? { width: 0, height: 0, left: 0, top: 0 };
mouseX.set(clientX - (left + width / 2));
mouseY.set(clientY - (top + height / 2));
};
return (
<motion.div ref={cardRef} drag dragConstraints={dragConstraintsRef} onDrag={handleDrag} onDragStart={() => lastVelocity.current = {x:0, y:0}} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98, cursor: 'grabbing' }} onMouseMove={handleMouseMove} onMouseLeave={() => { mouseX.set(0); mouseY.set(0);}} style={{rotateX, rotateY}} className="relative bg-neutral-100 !p-4 !pb-16 flex flex-col items-center justify-start aspect-[3/4] w-80 max-w-full rounded-md shadow-lg [transform-style:preserve-3d]">
{cardInnerContent}
</motion.div>
);
};
const Footer = () => {
return (
<footer className="fixed bottom-0 left-0 right-0 bg-black/50 backdrop-blur-sm p-3 z-50 text-neutral-300 text-xs sm:text-sm border-t border-white/10">
<div className="max-w-screen-xl mx-auto flex justify-center items-center gap-4 px-4">
<p>
ساخته شده توسط{' '}
<a href="#" target="_blank" rel="noopener noreferrer" className="text-neutral-400 hover:text-yellow-400 transition-colors duration-200">
هوش مصنوعی آلفا
</a>
{' '}
</p>
</div>
</footer>
);
};
// --- بخش ۳: کامپوننت اصلی اپلیکیشن ---
function App() {
const [uploadedImage, setUploadedImage] = useState(null);
const [generatedImages, setGeneratedImages] = useState({});
const [isDownloading, setIsDownloading] = useState(false);
const [appState, setAppState] = useState('idle');
const dragAreaRef = useRef(null);
const isMobile = window.innerWidth <= 768;
const DECADES_FA = ['دهه ۱۹۵۰', 'دهه ۱۹۶۰', 'دهه ۱۹۷۰', 'دهه ۱۹۸۰', 'دهه ۱۹۹۰', 'دهه ۲۰۰۰'];
const DECADES_EN = ['1950s', '1960s', '1970s', '1980s', '1990s', '2000s'];
const POSITIONS = [ { top: '5%', left: '10%', rotate: -8 }, { top: '15%', left: '60%', rotate: 5 }, { top: '45%', left: '5%', rotate: 3 }, { top: '2%', left: '35%', rotate: 10 }, { top: '40%', left: '70%', rotate: -12 }, { top: '50%', left: '38%', rotate: -3 }, ];
const GHOST_CONFIG = [ { initial: { x: "-150%", y: "-100%", rotate: -30 }, transition: { delay: 0.2 } }, { initial: { x: "150%", y: "-80%", rotate: 25 }, transition: { delay: 0.4 } }, { initial: { x: "-120%", y: "120%", rotate: 45 }, transition: { delay: 0.6 } }, { initial: { x: "180%", y: "90%", rotate: -20 }, transition: { delay: 0.8 } }, ];
const primaryButtonClasses = "font-bold text-xl text-center text-black bg-yellow-400 py-3 px-8 rounded-sm transform transition-transform duration-200 hover:scale-105 hover:rotate-2 hover:bg-yellow-300 shadow-[-2px_2px_0px_2px_rgba(0,0,0,0.2)]";
const secondaryButtonClasses = "font-bold text-xl text-center text-white bg-white/10 backdrop-blur-sm border-2 border-white/80 py-3 px-8 rounded-sm transform transition-transform duration-200 hover:scale-105 hover:-rotate-2 hover:bg-white hover:text-black";
const handleImageUpload = (e) => {
if (e.target.files && e.target.files[0]) {
const reader = new FileReader();
reader.onloadend = () => {
setUploadedImage(reader.result); setAppState('image-uploaded'); setGeneratedImages({});
};
reader.readAsDataURL(e.target.files[0]);
}
};
const generateImageWithFallback = async (decade) => {
try {
console.log(`تلاش برای ساخت تصویر ${decade} با پرامپت اصلی...`);
const primaryPrompt = getPrimaryPrompt(decade);
const resultUrl = await generateDecadeImage(uploadedImage, primaryPrompt);
setGeneratedImages(prev => ({ ...prev, [decade]: { status: 'done', url: resultUrl } }));
} catch (err) {
console.warn(`پرامپت اصلی برای ${decade} ناموفق بود. تلاش با پرامپت جایگزین...`);
try {
const fallbackPrompt = getFallbackPrompt(decade);
const resultUrl = await generateDecadeImage(uploadedImage, fallbackPrompt);
setGeneratedImages(prev => ({ ...prev, [decade]: { status: 'done', url: resultUrl } }));
} catch (fallbackErr) {
console.error(`پرامپت جایگزین هم برای ${decade} ناموفق بود:`, fallbackErr);
setGeneratedImages(prev => ({ ...prev, [decade]: { status: 'error', error: fallbackErr.message } }));
}
}
};
const handleGenerateClick = async () => {
if (!uploadedImage) return;
setAppState('generating');
let initialImages = {};
DECADES_EN.forEach(decade => initialImages[decade] = { status: 'pending' });
setGeneratedImages(initialImages);
const concurrencyLimit = 2;
const decadesQueue = [...DECADES_EN];
const workers = Array(concurrencyLimit).fill(null).map(async () => {
while (decadesQueue.length > 0) {
const decade = decadesQueue.shift();
if (decade) await generateImageWithFallback(decade);
}
});
await Promise.all(workers);
setAppState('results-shown');
};
const handleRegenerateDecade = async (decadeFa) => {
if (!uploadedImage) return;
const decadeIndex = DECADES_FA.indexOf(decadeFa);
if (decadeIndex === -1) return;
const decadeEn = DECADES_EN[decadeIndex];
if (generatedImages[decadeEn]?.status === 'pending') return;
setGeneratedImages(prev => ({ ...prev, [decadeEn]: { status: 'pending' } }));
await generateImageWithFallback(decadeEn);
};
// *** شروع تغییرات ***
const handleDownloadIndividualImage = (decadeFa) => {
const decadeIndex = DECADES_FA.indexOf(decadeFa);
const decadeEn = DECADES_EN[decadeIndex];
const image = generatedImages[decadeEn];
if (image?.status === 'done' && image.url) {
const filename = `safar-dar-zaman-${decadeEn}.jpg`;
// به جای دانلود مستقیم، پیام را به والد ارسال می‌کنیم
sendDownloadRequestToParent(image.url, filename);
}
};
const handleDownloadAlbum = async () => {
setIsDownloading(true);
try {
const imageData = DECADES_EN.reduce((acc, decade, index) => {
if (generatedImages[decade]?.status === 'done' && generatedImages[decade].url) {
acc[DECADES_FA[index]] = generatedImages[decade].url;
} return acc;
}, {});
if (Object.keys(imageData).length === 0) {
alert("هنوز تصویری برای ساخت آلبوم آماده نیست."); return;
}
const albumDataUrl = await createAlbumPage(imageData);
const filename = 'album-safar-dar-zaman.jpg';
// به جای دانلود مستقیم، پیام را به والد ارسال می‌کنیم
sendDownloadRequestToParent(albumDataUrl, filename);
} catch (error) {
console.error("خطا در ساخت آلبوم:", error);
alert("متاسفانه در ساخت آلبوم خطایی رخ داد.");
} finally { setIsDownloading(false); }
};
// *** پایان تغییرات ***
const handleReset = () => { setUploadedImage(null); setGeneratedImages({}); setAppState('idle'); };
return (
<main className="bg-black text-neutral-200 min-h-screen w-full flex flex-col items-center justify-center p-4 pb-24 overflow-hidden relative">
<div className="absolute top-0 left-0 w-full h-full bg-grid-white/[0.05]"></div>
<div className="z-10 flex flex-col items-center justify-center w-full h-full flex-1 min-h-0">
<div className="text-center mb-10">
<h1 className="text-6xl md:text-8xl font-caveat font-bold text-neutral-100">سفر در زمان</h1>
<p className="font-bold text-neutral-300 mt-2 text-2xl tracking-wide">خودت رو در دهه‌های مختلف بازسازی کن</p>
</div>
{appState === 'idle' && (
<div className="relative flex flex-col items-center justify-center w-full">
{GHOST_CONFIG.map((config, index) => <motion.div key={index} className="absolute w-80 h-[26rem] rounded-md p-4 bg-neutral-100/10 blur-sm" initial={config.initial} animate={{ x: "0%", y: "0%", rotate: (Math.random() - 0.5) * 20, scale: 0, opacity: 0 }} transition={{ ...config.transition, ease: "circOut", duration: 2 }} />)}
<motion.div initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: 1.5, duration: 0.8, type: 'spring' }} className="flex flex-col items-center">
<label htmlFor="file-upload" className="cursor-pointer group transform hover:scale-105 transition-transform duration-300">
<PolaroidCard caption="برای شروع کلیک کنید" status="done"/>
</label>
<input id="file-upload" type="file" className="hidden" accept="image/png, image/jpeg, image/webp" onChange={handleImageUpload} />
<p className="mt-8 font-bold text-neutral-500 text-center max-w-xs text-xl">برای شروع سفر در زمان، عکست رو آپلود کن.</p>
</motion.div>
</div>
)}
{appState === 'image-uploaded' && uploadedImage && (
<div className="flex flex-col items-center gap-6">
<PolaroidCard imageUrl={uploadedImage} caption="عکس شما" status="done" />
<div className="flex items-center gap-4 mt-4">
<button onClick={handleReset} className={secondaryButtonClasses}>عکس دیگر</button>
<button onClick={handleGenerateClick} className={primaryButtonClasses}>بساز!</button>
</div>
</div>
)}
{(appState === 'generating' || appState === 'results-shown') && (
<React.Fragment>
<div ref={dragAreaRef} className={`relative w-full max-w-5xl h-[600px] mt-4 ${isMobile ? 'hidden' : 'block'}`}>
{DECADES_EN.map((decade, index) => (
<motion.div key={decade} className="absolute cursor-grab active:cursor-grabbing" style={POSITIONS[index]} initial={{ opacity: 0, scale: 0.5, y: 100, rotate: 0 }} animate={{ opacity: 1, scale: 1, y: 0, rotate: `${POSITIONS[index].rotate}deg` }} transition={{ type: 'spring', stiffness: 100, damping: 20, delay: index * 0.15 }}>
<PolaroidCard dragConstraintsRef={dragAreaRef} caption={DECADES_FA[index]} status={generatedImages[decade]?.status || 'pending'} imageUrl={generatedImages[decade]?.url} error={generatedImages[decade]?.error} onShake={handleRegenerateDecade} onDownload={handleDownloadIndividualImage} isMobile={false} />
</motion.div>
))}
</div>
<div className={`w-full max-w-sm flex-1 overflow-y-auto mt-4 space-y-8 p-4 ${!isMobile ? 'hidden' : 'block'}`}>
{DECADES_EN.map((decade, index) => <div key={decade} className="flex justify-center"><PolaroidCard caption={DECADES_FA[index]} status={generatedImages[decade]?.status || 'pending'} imageUrl={generatedImages[decade]?.url} error={generatedImages[decade]?.error} onShake={handleRegenerateDecade} onDownload={handleDownloadIndividualImage} isMobile={true} /></div>)}
</div>
<div className="h-20 mt-4 flex items-center justify-center">
{appState === 'generating' && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-center"
>
<p className="text-2xl font-bold text-neutral-300">در حال ساخت تصاویر...</p>
<p className="text-lg text-neutral-400">ماشین زمان در حال کار است، لطفاً کمی صبر کنید.</p>
</motion.div>
)}
{appState === 'results-shown' && (
<div className="flex flex-col sm:flex-row items-center gap-4">
<button onClick={handleDownloadAlbum} disabled={isDownloading} className={`${primaryButtonClasses} disabled:opacity-50 disabled:cursor-not-allowed`}>
{isDownloading ? 'در حال ساخت...' : 'دانلود آلبوم'}
</button>
<button onClick={handleReset} className={secondaryButtonClasses}>شروع مجدد</button>
</div>
)}
</div>
</React.Fragment>
)}
</div>
<Footer />
</main>
);
}
// --- بخش ۴: رندر کردن اپلیکیشن ---
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);
root.render(<StrictMode><App /></StrictMode>);
</script>
</body>
</html>