| <!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> |
| |
| <script src="https://cdn.tailwindcss.com"></script> |
| |
| <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> |
|
|
| |
| <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; |
| |
| |
| |
| 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.`; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| function sendDownloadRequestToParent(url, fallbackFilename) { |
| |
| 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); |
| } |
| } |
| |
| |
| |
| |
| 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> |