const data = await response.json(); const solPrice = data.data.SOL.price; let ficosPrice = null; try { const ficosResponse = await fetch(`https://lite-api.jup.ag/price/v3/price?ids=${FICOS_MINT.toString()}`); const ficosData = await ficosResponse.json(); ficosPrice = ficosData.data[FICOS_MINT.toString()]?.price; } catch (e) { console.log('FICOS price not available on Jupiter, using fallback'); } const solCost = SOL_MINT_FEE; let ficosCost = null; if (ficosPrice && ficosPrice > 0) { const solUsdValue = solCost * solPrice; const ficosUsdValue = solUsdValue * 0.5; // 50% discount ficosCost = Math.ceil(ficosUsdValue / ficosPrice); } return { solPrice, solCost, ficosPrice, ficosCost }; } catch (error) { console.error('Failed to fetch prices:', error); return { solPrice: null, solCost: SOL_MINT_FEE, ficosPrice: null, ficosCost: null }; } }; // =================================================================================== // --- REACT APPLICATION --- // =================================================================================== const AppContext = createContext(); const AppProvider = ({ children }) => { const [page, setPage] = useState('home'); const [selectedTributeId, setSelectedTributeId] = useState(null); const [tributes, setTributes] = useState(initialMockTributes); const navigate = (newPage, tributeId = null) => { setPage(newPage); setSelectedTributeId(tributeId); }; const addTribute = (newTribute) => setTributes(prev => [...prev, newTribute]); return {children}; }; const HomePage = () => { const { navigate } = useContext(AppContext); const connectWallet = async () => { try { const { solana } = window; if (solana && solana.isPhantom) { const response = await solana.connect(); console.log('Wallet connected:', response.publicKey.toString()); } else { window.open('https://phantom.app/', '_blank'); } } catch (error) { console.error('Wallet connection failed:', error); } }; const [hoveredButton, setHoveredButton] = useState(null); const containerRef = useRef(null); const hotspots = { raven: { x1: 600, y1: 700, x2: 800, y2: 900, action: () => navigate('mint') }, portrait: { x1: 800, y1: 300, x2: 1100, y2: 700, action: () => navigate('garden') }, wallet: { x1: 1100, y1: 800, x2: 1350, y2: 950, action: () => connectWallet() }, skull: { x1: 900, y1: 800, x2: 1050, y2: 950, action: () => window.location.href = '/' }, }; const nativeImageDimensions = { width: 1920, height: 1080 }; const checkHotspots = (e) => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const scaleX = rect.width / nativeImageDimensions.width; const scaleY = rect.height / nativeImageDimensions.height; const scale = Math.min(scaleX, scaleY); const renderedWidth = nativeImageDimensions.width * scale; const renderedHeight = nativeImageDimensions.height * scale; const offsetX = (rect.width - renderedWidth) / 2; const offsetY = (rect.height - renderedHeight) / 2; const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const x = mouseX - offsetX; const y = mouseY - offsetY; for (const key in hotspots) { const spot = hotspots[key]; if ( x > spot.x1 * scale && x < spot.x2 * scale && y > spot.y1 * scale && y < spot.y2 * scale) { setHoveredButton(key); return; } } setHoveredButton(null); }; const handleHotspotClick = (e) => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const scaleX = rect.width / nativeImageDimensions.width; const scaleY = rect.height / nativeImageDimensions.height; const scale = Math.min(scaleX, scaleY); const renderedWidth = nativeImageDimensions.width * scale; const renderedHeight = nativeImageDimensions.height * scale; const offsetX = (rect.width - renderedWidth) / 2; const offsetY = (rect.height - renderedHeight) / 2; const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const x = mouseX - offsetX; const y = mouseY - offsetY; for (const key in hotspots) { const spot = hotspots[key]; if ( x > spot.x1 * scale && x < spot.x2 * scale && y > spot.y1 * scale && y < spot.y2 * scale) { spot.action(); return; } } }; return (
Ofrenda background Memento Mori
setHoveredButton(null)} onClick={handleHotspotClick} >
); }; const PageNavigation = ({ toMint, toGarden, toHome, showPriceBanner }) => { const { navigate } = useContext(AppContext); const [walletAddress, setWalletAddress] = useState(null); const [prices, setPrices] = useState(null); useEffect(() => { const checkWalletConnection = async () => { const { solana } = window; if (solana && solana.isPhantom && solana.isConnected) { setWalletAddress(solana.publicKey?.toString()); } }; checkWalletConnection(); if (window.solana) { window.solana.on('connect', () => setWalletAddress(window.solana.publicKey?.toString())); window.solana.on('disconnect', () => setWalletAddress(null)); } if (showPriceBanner) { const loadPrices = async () => setPrices(await fetchTokenPrices()); loadPrices(); const interval = setInterval(loadPrices, 60000); return () => clearInterval(interval); } }, [showPriceBanner]); const handleWalletClick = async () => { try { const { solana } = window; if (!solana || !solana.isPhantom) { window.open('https://phantom.app/', '_blank'); return; } if (walletAddress) { await solana.disconnect(); } else { await solana.connect(); } } catch (error) { console.error('Wallet interaction failed:', error); } }; return (
{toHome && ( )} {toMint && ( )} {toGarden && ( )}
{showPriceBanner && prices && (
Mint:
SOL:{prices.solCost}
|
-50%FICOS:{prices.ficosCost ? ({prices.ficosCost.toLocaleString()}) : (N/A)}
Buy
)}
); }; const KeyWarningModal = ({ onAgree }) => (

Important Notice: Encryption Key

All encryption keys are generated and stored **locally on your device**.

Your messages will be **permanently lost** if your encryption key is lost. We do not have access to your key, your device, or your messages. We cannot recover or replace lost keys.

By proceeding, you forfeit any and all claims against this project for lost or stolen keys. We **highly suggest** backing up your key on offline, locked storage media, the same as you would a wallet seed phrase.

); const MintPage = () => { const { navigate, addTribute } = useContext(AppContext); const [formData, setFormData] = useState({ name: '', relationship: '', identity: '', spiritAnimal: '', hobbies: '', family: '', personality: '', eulogy: '', encryptedMessage: '' }); const [unlockDate, setUnlockDate] = useState(''); const [uploadedImageBase64, setUploadedImageBase64] = useState(null); const [uploadedImageName, setUploadedImageName] = useState(null); const [showKeyWarning, setShowKeyWarning] = useState(false); const [hasAcknowledged, setHasAcknowledged] = useState(() => localStorage.getItem('keyWarningAcknowledged') === 'true'); const [generatedArtUrl, setGeneratedArtUrl] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isMinting, setIsMinting] = useState(false); const handleEncryptedMessageFocus = () => !hasAcknowledged && setShowKeyWarning(true); const handleAcknowledge = () => { localStorage.setItem('keyWarningAcknowledged', 'true'); setHasAcknowledged(true); setShowKeyWarning(false); }; const handleInputChange = (e) => setFormData(prev => ({ ...prev, [e.target.name]: e.target.value })); const handleImageUpload = (e) => { const file = e.target.files[0]; if (file) { setUploadedImageName(file.name); const reader = new FileReader(); reader.onload = (loadEvent) => { const base64String = loadEvent.target.result.split(',')[1]; setUploadedImageBase64(base64String); }; reader.readAsDataURL(file); } }; const handleSuggestEulogy = async () => { const prompt = `Write a short, heartfelt eulogy (maximum 2-3 sentences) for my ${formData.relationship}, ${formData.name}, who identified as a ${formData.identity}. They were known for being ${formData.personality} and loved ${formData.hobbies}.`; const suggestedEulogy = await generateTextWithGemini(prompt); setFormData(prev => ({...prev, eulogy: suggestedEulogy})); } const handleGeneratePreview = async () => { const PREVIEW_LIMIT = 5; const ONE_HOUR = 3600000; const now = Date.now(); let timestamps = JSON.parse(localStorage.getItem('previewTimestamps') || '[]'); timestamps = timestamps.filter(ts => now - ts < ONE_HOUR); if (timestamps.length >= PREVIEW_LIMIT) { alert("You have reached the preview limit. Please try again in an hour."); return; } timestamps.push(now); localStorage.setItem('previewTimestamps', JSON.stringify(timestamps)); setIsLoading(true); const artUrl = await generateArtWithGemini(formData, uploadedImageBase64); setGeneratedArtUrl(artUrl); setIsLoading(false); }; const handleSubmit = async (e) => { e.preventDefault(); if (!generatedArtUrl) return; setIsMinting(true); const inscriptionData = { ...formData, imageUrl: generatedArtUrl, unlockDate: unlockDate, encryptedMessage: formData.encryptedMessage ? encryptMessage(formData.encryptedMessage) : null }; const success = await inscribeOnchain(inscriptionData); if (success) { addTribute({ id: Date.now().toString(), ...inscriptionData }); navigate('garden'); } else { console.error("Inscription failed."); } setIsMinting(false); }; return ( <> {showKeyWarning && }

The Honored Soul

Offerings of Memory