main: added inital version of music shop
Signed-off-by: nfel <nfilsaraee@gmail.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FaPlay, FaShoppingCart, FaInfoCircle } from 'react-icons/fa';
|
||||
import { Album } from '@/lib/types';
|
||||
import { useCart } from '@/lib/CartContext';
|
||||
|
||||
interface AlbumCardProps {
|
||||
album: Album;
|
||||
isPurchased: boolean;
|
||||
onPlay: (album: Album) => void;
|
||||
onPurchase: (album: Album) => void;
|
||||
}
|
||||
|
||||
export default function AlbumCard({ album, isPurchased, onPlay, onPurchase }: AlbumCardProps) {
|
||||
const router = useRouter();
|
||||
const { addToCart, isInCart } = useCart();
|
||||
|
||||
const handleAddToCart = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!isInCart(album.id) && !isPurchased) {
|
||||
addToCart(album);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetails = (e?: React.MouseEvent) => {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
router.push(`/album/${album.id}`);
|
||||
};
|
||||
|
||||
const handleCardClick = () => {
|
||||
router.push(`/album/${album.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
whileHover={{ y: -10 }}
|
||||
onClick={handleCardClick}
|
||||
className="glass-effect rounded-xl overflow-hidden group cursor-pointer"
|
||||
>
|
||||
{/* Album Cover */}
|
||||
<div className="relative aspect-square bg-gradient-to-br from-primary-600/50 to-primary-800/50 flex items-center justify-center overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-accent-cyan/20 to-accent-orange/20"></div>
|
||||
<div className="relative z-10 text-6xl font-bold text-white/20 p-8 text-center">
|
||||
{album.title}
|
||||
</div>
|
||||
|
||||
{/* Overlay on hover */}
|
||||
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-all duration-300 flex items-center justify-center gap-3">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onPlay(album);
|
||||
}}
|
||||
className="p-4 bg-accent-cyan hover:bg-accent-cyan/80 rounded-full transition-all glow-cyan"
|
||||
aria-label="Play preview"
|
||||
>
|
||||
<FaPlay className="text-2xl text-white" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => handleViewDetails(e)}
|
||||
className="p-4 bg-white/20 hover:bg-white/30 rounded-full transition-all"
|
||||
aria-label="View details"
|
||||
>
|
||||
<FaInfoCircle className="text-2xl text-white" />
|
||||
</button>
|
||||
{!isPurchased && (
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
disabled={isInCart(album.id)}
|
||||
className={`p-4 ${
|
||||
isInCart(album.id)
|
||||
? 'bg-gray-700 cursor-not-allowed'
|
||||
: 'bg-accent-orange hover:bg-accent-orange/80 glow-orange'
|
||||
} rounded-full transition-all`}
|
||||
aria-label="Add to cart"
|
||||
>
|
||||
<FaShoppingCart className="text-2xl text-white" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Purchased Badge */}
|
||||
{isPurchased && (
|
||||
<div className="absolute top-4 right-4 bg-accent-cyan text-white px-3 py-1 rounded-full text-sm font-semibold">
|
||||
Owned
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Album Info */}
|
||||
<div className="p-6 space-y-3">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white group-hover:text-accent-cyan transition-colors">
|
||||
{album.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-400">{album.year} • {album.genre}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-300 text-sm line-clamp-2">{album.description}</p>
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<span className="text-sm text-gray-400">{album.songs.length} tracks</span>
|
||||
{!isPurchased && (
|
||||
<span className="text-lg font-bold text-accent-orange">${album.price}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import AlbumCard from './AlbumCard';
|
||||
import { Album } from '@/lib/types';
|
||||
import { albums } from '@/lib/data';
|
||||
|
||||
interface AlbumShowcaseProps {
|
||||
purchasedAlbums: string[];
|
||||
onPlay: (album: Album) => void;
|
||||
onPurchase: (album: Album) => void;
|
||||
}
|
||||
|
||||
export default function AlbumShowcase({ purchasedAlbums, onPlay, onPurchase }: AlbumShowcaseProps) {
|
||||
return (
|
||||
<section className="py-20 px-4 md:px-8" id="albums">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-accent-cyan to-accent-orange mb-4">
|
||||
Discography
|
||||
</h2>
|
||||
<p className="text-xl text-gray-300 max-w-2xl mx-auto">
|
||||
Explore a collection of progressive rock albums that push the boundaries of sound and creativity
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{albums.map((album) => (
|
||||
<AlbumCard
|
||||
key={album.id}
|
||||
album={album}
|
||||
isPurchased={purchasedAlbums.includes(album.id)}
|
||||
onPlay={onPlay}
|
||||
onPurchase={onPurchase}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { FaSpotify, FaYoutube, FaInstagram } from 'react-icons/fa';
|
||||
import { SiApplemusic } from 'react-icons/si';
|
||||
import { artistBio } from '@/lib/data';
|
||||
|
||||
export default function Biography() {
|
||||
return (
|
||||
<section className="py-20 px-4 md:px-8" id="bio">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="glass-effect rounded-2xl p-8 md:p-12"
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||
{/* Image Section */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative"
|
||||
>
|
||||
<div className="aspect-square rounded-xl overflow-hidden bg-gradient-to-br from-accent-orange/20 to-accent-cyan/20 flex items-center justify-center border-2 border-accent-cyan/50 glow-cyan">
|
||||
<div className="text-6xl md:text-8xl font-bold text-accent-cyan/30">
|
||||
{artistBio.name}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Bio Section */}
|
||||
<div className="space-y-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-accent-cyan to-accent-orange mb-2">
|
||||
{artistBio.name}
|
||||
</h1>
|
||||
<p className="text-xl text-accent-cyan/80 mb-6">{artistBio.title}</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
viewport={{ once: true }}
|
||||
className="space-y-4 text-gray-300 leading-relaxed"
|
||||
>
|
||||
{artistBio.bio.split('\n\n').map((paragraph, idx) => (
|
||||
<p key={idx}>{paragraph}</p>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Social Links */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
className="flex gap-4 pt-6"
|
||||
>
|
||||
<a
|
||||
href={artistBio.socialLinks.spotify}
|
||||
className="p-3 glass-effect rounded-lg hover:bg-accent-cyan/20 transition-all hover:glow-cyan"
|
||||
aria-label="Spotify"
|
||||
>
|
||||
<FaSpotify className="text-2xl text-accent-cyan" />
|
||||
</a>
|
||||
<a
|
||||
href={artistBio.socialLinks.youtube}
|
||||
className="p-3 glass-effect rounded-lg hover:bg-accent-orange/20 transition-all hover:glow-orange"
|
||||
aria-label="YouTube"
|
||||
>
|
||||
<FaYoutube className="text-2xl text-accent-orange" />
|
||||
</a>
|
||||
<a
|
||||
href={artistBio.socialLinks.instagram}
|
||||
className="p-3 glass-effect rounded-lg hover:bg-accent-cyan/20 transition-all hover:glow-cyan"
|
||||
aria-label="Instagram"
|
||||
>
|
||||
<FaInstagram className="text-2xl text-accent-cyan" />
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="p-3 glass-effect rounded-lg hover:bg-accent-orange/20 transition-all hover:glow-orange"
|
||||
aria-label="Apple Music"
|
||||
>
|
||||
<SiApplemusic className="text-2xl text-accent-orange" />
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { FaTimes, FaTrash, FaShoppingCart } from 'react-icons/fa';
|
||||
import { useCart } from '@/lib/CartContext';
|
||||
import { Album, Purchase } from '@/lib/types';
|
||||
import PaymentModal from './PaymentModal';
|
||||
import PurchaseSuccessModal from './PurchaseSuccessModal';
|
||||
|
||||
interface CartSidebarProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function CartSidebar({ isOpen, onClose }: CartSidebarProps) {
|
||||
const { cartItems, removeFromCart, clearCart, getCartTotal } = useCart();
|
||||
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
||||
const [albumToPurchase, setAlbumToPurchase] = useState<Album | null>(null);
|
||||
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||
const [latestPurchase, setLatestPurchase] = useState<Purchase | null>(null);
|
||||
const [purchasedAlbums, setPurchasedAlbums] = useState<string[]>([]);
|
||||
|
||||
const handleCheckout = () => {
|
||||
if (cartItems.length === 0) return;
|
||||
|
||||
// For simplicity, we'll purchase the first item
|
||||
// In a real app, you'd handle multiple items differently
|
||||
if (cartItems.length > 0) {
|
||||
setAlbumToPurchase(cartItems[0].album);
|
||||
setShowPaymentModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePurchaseSuccess = (albumId: string, transactionId: string) => {
|
||||
const purchase: Purchase = {
|
||||
albumId,
|
||||
transactionId,
|
||||
purchaseDate: new Date(),
|
||||
};
|
||||
|
||||
// Load existing purchases
|
||||
const savedPurchases = localStorage.getItem('purchases');
|
||||
const existingPurchases = savedPurchases ? JSON.parse(savedPurchases) : [];
|
||||
const updatedPurchases = [...existingPurchases, purchase];
|
||||
|
||||
localStorage.setItem('purchases', JSON.stringify(updatedPurchases));
|
||||
setPurchasedAlbums([...purchasedAlbums, albumId]);
|
||||
setLatestPurchase(purchase);
|
||||
|
||||
// Remove from cart
|
||||
removeFromCart(albumId);
|
||||
|
||||
setShowPaymentModal(false);
|
||||
setShowSuccessModal(true);
|
||||
};
|
||||
|
||||
const handleCloseSuccessModal = () => {
|
||||
setShowSuccessModal(false);
|
||||
setAlbumToPurchase(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40"
|
||||
/>
|
||||
|
||||
{/* Sidebar */}
|
||||
<motion.div
|
||||
initial={{ x: '100%' }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: '100%' }}
|
||||
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
|
||||
className="fixed right-0 top-0 bottom-0 w-full md:w-96 glass-effect border-l border-white/10 z-50 flex flex-col"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<FaShoppingCart className="text-2xl text-accent-cyan" />
|
||||
<h2 className="text-2xl font-bold text-white">
|
||||
Cart ({cartItems.length})
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-white/10 rounded-full transition-colors"
|
||||
aria-label="Close cart"
|
||||
>
|
||||
<FaTimes className="text-xl text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Cart Items */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{cartItems.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<FaShoppingCart className="text-6xl text-gray-600 mb-4" />
|
||||
<p className="text-gray-400 text-lg">Your cart is empty</p>
|
||||
<p className="text-gray-500 text-sm mt-2">Add some albums to get started</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{cartItems.map((item) => (
|
||||
<motion.div
|
||||
key={item.album.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="glass-effect rounded-xl p-4"
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
{/* Album Cover */}
|
||||
<div className="w-20 h-20 rounded-lg bg-gradient-to-br from-accent-cyan/20 to-accent-orange/20 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-xs text-white/50 font-bold text-center px-2">
|
||||
{item.album.title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Album Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-white font-semibold truncate">
|
||||
{item.album.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
{item.album.year} • {item.album.songs.length} tracks
|
||||
</p>
|
||||
<p className="text-accent-orange font-bold mt-2">
|
||||
${item.album.price}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Remove Button */}
|
||||
<button
|
||||
onClick={() => removeFromCart(item.album.id)}
|
||||
className="p-2 hover:bg-red-500/20 rounded-lg transition-colors self-start"
|
||||
aria-label="Remove from cart"
|
||||
>
|
||||
<FaTrash className="text-red-400" />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
{cartItems.length > 0 && (
|
||||
<div className="border-t border-white/10 p-6 space-y-4">
|
||||
{/* Total */}
|
||||
<div className="flex items-center justify-between text-xl">
|
||||
<span className="text-gray-300 font-semibold">Total</span>
|
||||
<span className="text-accent-orange font-bold">
|
||||
${getCartTotal().toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={handleCheckout}
|
||||
className="w-full py-4 bg-gradient-to-r from-accent-orange to-accent-orange/80 hover:from-accent-orange/90 hover:to-accent-orange/70 rounded-lg font-semibold text-white transition-all glow-orange"
|
||||
>
|
||||
Proceed to Checkout
|
||||
</button>
|
||||
<button
|
||||
onClick={clearCart}
|
||||
className="w-full py-3 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg font-semibold text-gray-300 transition-all"
|
||||
>
|
||||
Clear Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Payment Modal */}
|
||||
<PaymentModal
|
||||
album={albumToPurchase}
|
||||
onClose={() => {
|
||||
setShowPaymentModal(false);
|
||||
setAlbumToPurchase(null);
|
||||
}}
|
||||
onSuccess={handlePurchaseSuccess}
|
||||
/>
|
||||
|
||||
{/* Purchase Success Modal */}
|
||||
<PurchaseSuccessModal
|
||||
show={showSuccessModal}
|
||||
album={albumToPurchase}
|
||||
purchase={latestPurchase}
|
||||
onClose={handleCloseSuccessModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { FaMusic, FaShoppingCart } from 'react-icons/fa';
|
||||
import { useCart } from '@/lib/CartContext';
|
||||
|
||||
interface HeaderProps {
|
||||
onCartClick?: () => void;
|
||||
}
|
||||
|
||||
export default function Header({ onCartClick }: HeaderProps) {
|
||||
const { getCartCount } = useCart();
|
||||
const cartCount = getCartCount();
|
||||
|
||||
const scrollToSection = (id: string) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.header
|
||||
initial={{ y: -100 }}
|
||||
animate={{ y: 0 }}
|
||||
className="fixed top-0 left-0 right-0 z-40 glass-effect border-b border-white/10"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 md:px-8 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Logo */}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className="flex items-center gap-3 cursor-pointer"
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
>
|
||||
<div className="p-2 bg-gradient-to-br from-accent-cyan to-accent-orange rounded-lg glow-cyan">
|
||||
<FaMusic className="text-2xl text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-accent-cyan to-accent-orange">
|
||||
Parsa
|
||||
</h1>
|
||||
<p className="text-xs text-gray-400">Progressive Rock</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex items-center gap-6">
|
||||
<div className="hidden md:flex items-center gap-8">
|
||||
<button
|
||||
onClick={() => scrollToSection('bio')}
|
||||
className="text-gray-300 hover:text-accent-cyan transition-colors"
|
||||
>
|
||||
Bio
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scrollToSection('albums')}
|
||||
className="text-gray-300 hover:text-accent-cyan transition-colors"
|
||||
>
|
||||
Albums
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Cart Button */}
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={onCartClick}
|
||||
className="relative p-3 bg-white/10 hover:bg-white/20 rounded-lg transition-all"
|
||||
aria-label="Shopping cart"
|
||||
>
|
||||
<FaShoppingCart className="text-xl text-accent-cyan" />
|
||||
{cartCount > 0 && (
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
className="absolute -top-1 -right-1 bg-accent-orange text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center"
|
||||
>
|
||||
{cartCount}
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</motion.header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { FaPlay, FaPause, FaStepBackward, FaStepForward, FaTimes, FaChevronDown, FaChevronUp } from 'react-icons/fa';
|
||||
import { Album, Song } from '@/lib/types';
|
||||
|
||||
interface MusicPlayerProps {
|
||||
album: Album | null;
|
||||
isPurchased: boolean;
|
||||
onClose: () => void;
|
||||
onPurchase: (album: Album) => void;
|
||||
}
|
||||
|
||||
export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }: MusicPlayerProps) {
|
||||
const [currentSongIndex, setCurrentSongIndex] = useState(0);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [showTrackList, setShowTrackList] = useState(false);
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
|
||||
const currentSong = album?.songs[currentSongIndex];
|
||||
|
||||
useEffect(() => {
|
||||
if (album) {
|
||||
setCurrentSongIndex(0);
|
||||
setIsPlaying(false);
|
||||
setCurrentTime(0);
|
||||
}
|
||||
}, [album]);
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
|
||||
const updateTime = () => setCurrentTime(audio.currentTime);
|
||||
const updateDuration = () => setDuration(audio.duration);
|
||||
const handleEnded = () => {
|
||||
if (!isPurchased) {
|
||||
// Preview ended
|
||||
setIsPlaying(false);
|
||||
setCurrentTime(0);
|
||||
} else {
|
||||
// Move to next song
|
||||
handleNext();
|
||||
}
|
||||
};
|
||||
|
||||
audio.addEventListener('timeupdate', updateTime);
|
||||
audio.addEventListener('loadedmetadata', updateDuration);
|
||||
audio.addEventListener('ended', handleEnded);
|
||||
|
||||
return () => {
|
||||
audio.removeEventListener('timeupdate', updateTime);
|
||||
audio.removeEventListener('loadedmetadata', updateDuration);
|
||||
audio.removeEventListener('ended', handleEnded);
|
||||
};
|
||||
}, [isPurchased, currentSongIndex, album]);
|
||||
|
||||
const togglePlay = () => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
|
||||
if (isPlaying) {
|
||||
audio.pause();
|
||||
} else {
|
||||
audio.play();
|
||||
}
|
||||
setIsPlaying(!isPlaying);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (!album) return;
|
||||
const nextIndex = (currentSongIndex + 1) % album.songs.length;
|
||||
setCurrentSongIndex(nextIndex);
|
||||
setIsPlaying(false);
|
||||
setCurrentTime(0);
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (!album) return;
|
||||
const prevIndex = currentSongIndex === 0 ? album.songs.length - 1 : currentSongIndex - 1;
|
||||
setCurrentSongIndex(prevIndex);
|
||||
setIsPlaying(false);
|
||||
setCurrentTime(0);
|
||||
};
|
||||
|
||||
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio || !isPurchased) return; // Only allow seeking for purchased albums
|
||||
|
||||
const time = parseFloat(e.target.value);
|
||||
audio.currentTime = time;
|
||||
setCurrentTime(time);
|
||||
};
|
||||
|
||||
const formatTime = (time: number) => {
|
||||
if (isNaN(time)) return '0:00';
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
if (!album) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{album && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 bg-black/90 backdrop-blur-xl z-50"
|
||||
/>
|
||||
|
||||
{/* Player Modal */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9, y: 50 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: 50 }}
|
||||
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none"
|
||||
>
|
||||
<div className="w-full max-w-md pointer-events-auto relative">
|
||||
{/* Close Button - Top Right */}
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onClose}
|
||||
className="absolute -top-2 -right-2 z-10 p-3 bg-white/90 hover:bg-white rounded-full transition-colors shadow-xl"
|
||||
aria-label="Close player"
|
||||
>
|
||||
<FaTimes className="text-xl text-primary-900" />
|
||||
</motion.button>
|
||||
|
||||
{/* Album Artwork */}
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="relative aspect-square rounded-2xl overflow-hidden mb-6 shadow-2xl"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary-500 via-primary-700 to-primary-900"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-accent-cyan/30 to-accent-orange/30"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-4xl md:text-5xl font-bold text-white/30 text-center px-6">
|
||||
{album.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview Badge */}
|
||||
{!isPurchased && (
|
||||
<div className="absolute top-3 right-3 bg-accent-orange/90 backdrop-blur-sm text-white px-3 py-1 rounded-full text-xs font-semibold shadow-lg">
|
||||
Preview Mode
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Song Info */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="text-center mb-6"
|
||||
>
|
||||
<h2 className="text-xl md:text-2xl font-bold text-white mb-1">
|
||||
{currentSong?.title}
|
||||
</h2>
|
||||
<p className="text-base text-gray-400">
|
||||
{album.title}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Track {currentSongIndex + 1} of {album.songs.length}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="mb-5"
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={duration || 0}
|
||||
value={currentTime}
|
||||
onChange={handleSeek}
|
||||
disabled={!isPurchased}
|
||||
className="w-full h-1 bg-gray-700/50 rounded-full appearance-none cursor-pointer"
|
||||
style={{
|
||||
background: `linear-gradient(to right, #00d9ff ${(currentTime / (duration || 1)) * 100}%, rgba(255,255,255,0.1) ${(currentTime / (duration || 1)) * 100}%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-400 mt-1.5 font-mono">
|
||||
<span>{formatTime(currentTime)}</span>
|
||||
<span>{isPurchased ? formatTime(duration) : '0:30'}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Controls */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="flex items-center justify-center gap-6 mb-6"
|
||||
>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={handlePrevious}
|
||||
className="p-3 hover:bg-white/10 rounded-full transition-colors"
|
||||
aria-label="Previous track"
|
||||
>
|
||||
<FaStepBackward className="text-xl text-white" />
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={togglePlay}
|
||||
className="p-5 bg-white hover:bg-gray-100 rounded-full transition-all shadow-2xl"
|
||||
aria-label={isPlaying ? 'Pause' : 'Play'}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<FaPause className="text-2xl text-primary-900" />
|
||||
) : (
|
||||
<FaPlay className="text-2xl text-primary-900 ml-0.5" />
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={handleNext}
|
||||
className="p-3 hover:bg-white/10 rounded-full transition-colors"
|
||||
aria-label="Next track"
|
||||
>
|
||||
<FaStepForward className="text-xl text-white" />
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
|
||||
{/* Purchase Button */}
|
||||
{!isPurchased && (
|
||||
<motion.button
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => onPurchase(album)}
|
||||
className="w-full mb-3 py-3 bg-gradient-to-r from-accent-orange to-accent-orange/80 hover:from-accent-orange/90 hover:to-accent-orange/70 rounded-xl font-semibold text-white text-base transition-all shadow-xl glow-orange"
|
||||
>
|
||||
Purchase Full Album - ${album.price}
|
||||
</motion.button>
|
||||
)}
|
||||
|
||||
{/* Track List Toggle */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
>
|
||||
<button
|
||||
onClick={() => setShowTrackList(!showTrackList)}
|
||||
className="w-full py-2.5 bg-white/5 hover:bg-white/10 rounded-lg transition-colors flex items-center justify-center gap-2 text-gray-300 text-sm"
|
||||
>
|
||||
{showTrackList ? <FaChevronUp className="text-xs" /> : <FaChevronDown className="text-xs" />}
|
||||
<span className="font-medium">
|
||||
{showTrackList ? 'Hide' : 'Show'} Track List
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{showTrackList && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="mt-3 max-h-52 overflow-y-auto rounded-lg bg-white/5 p-2">
|
||||
<div className="space-y-0.5">
|
||||
{album.songs.map((song, index) => (
|
||||
<button
|
||||
key={song.id}
|
||||
onClick={() => {
|
||||
setCurrentSongIndex(index);
|
||||
setIsPlaying(false);
|
||||
setCurrentTime(0);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 rounded-md transition-all ${
|
||||
index === currentSongIndex
|
||||
? 'bg-accent-cyan/20 text-accent-cyan'
|
||||
: 'hover:bg-white/5 text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 font-mono w-5">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<span className="text-sm font-medium">{song.title}</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 font-mono">{song.duration}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
|
||||
{/* Hidden Audio Element */}
|
||||
<audio
|
||||
ref={audioRef}
|
||||
src={isPurchased ? currentSong?.fullUrl : currentSong?.previewUrl}
|
||||
onPlay={() => setIsPlaying(true)}
|
||||
onPause={() => setIsPlaying(false)}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { FaTimes, FaCreditCard, FaLock } from 'react-icons/fa';
|
||||
import { Album } from '@/lib/types';
|
||||
|
||||
interface PaymentModalProps {
|
||||
album: Album | null;
|
||||
onClose: () => void;
|
||||
onSuccess: (albumId: string, transactionId: string) => void;
|
||||
}
|
||||
|
||||
export default function PaymentModal({ album, onClose, onSuccess }: PaymentModalProps) {
|
||||
const [cardNumber, setCardNumber] = useState('');
|
||||
const [cardName, setCardName] = useState('');
|
||||
const [expiry, setExpiry] = useState('');
|
||||
const [cvv, setCvv] = useState('');
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const formatCardNumber = (value: string) => {
|
||||
const numbers = value.replace(/\s/g, '');
|
||||
const groups = numbers.match(/.{1,4}/g);
|
||||
return groups ? groups.join(' ') : numbers;
|
||||
};
|
||||
|
||||
const formatExpiry = (value: string) => {
|
||||
const numbers = value.replace(/\D/g, '');
|
||||
if (numbers.length >= 2) {
|
||||
return numbers.slice(0, 2) + '/' + numbers.slice(2, 4);
|
||||
}
|
||||
return numbers;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
// Basic validation
|
||||
if (cardNumber.replace(/\s/g, '').length !== 16) {
|
||||
setError('Invalid card number');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cardName.trim()) {
|
||||
setError('Card name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (expiry.length !== 5) {
|
||||
setError('Invalid expiry date');
|
||||
return;
|
||||
}
|
||||
|
||||
if (cvv.length !== 3 && cvv.length !== 4) {
|
||||
setError('Invalid CVV');
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
|
||||
// Simulate payment processing
|
||||
setTimeout(() => {
|
||||
// Generate a mock transaction ID
|
||||
const transactionId = 'TXN-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9).toUpperCase();
|
||||
|
||||
setProcessing(false);
|
||||
if (album) {
|
||||
onSuccess(album.id, transactionId);
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
if (!album) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{album && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="glass-effect rounded-2xl max-w-md w-full p-8 border-2 border-accent-cyan/30"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-accent-cyan to-accent-orange">
|
||||
Purchase Album
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-white/10 rounded-full transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<FaTimes className="text-xl text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Album Info */}
|
||||
<div className="mb-6 p-4 bg-white/5 rounded-lg">
|
||||
<h3 className="font-semibold text-white">{album.title}</h3>
|
||||
<p className="text-sm text-gray-400">{album.songs.length} tracks • {album.genre}</p>
|
||||
<p className="text-2xl font-bold text-accent-orange mt-2">${album.price}</p>
|
||||
</div>
|
||||
|
||||
{/* Payment Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Card Number */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
<FaCreditCard className="inline mr-2" />
|
||||
Card Number
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={cardNumber}
|
||||
onChange={(e) => {
|
||||
const formatted = formatCardNumber(e.target.value.replace(/\D/g, '').slice(0, 16));
|
||||
setCardNumber(formatted);
|
||||
}}
|
||||
placeholder="1234 5678 9012 3456"
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:border-accent-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 text-white placeholder-gray-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Card Name */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Name on Card
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={cardName}
|
||||
onChange={(e) => setCardName(e.target.value.toUpperCase())}
|
||||
placeholder="JOHN DOE"
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:border-accent-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 text-white placeholder-gray-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Expiry and CVV */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Expiry Date
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={expiry}
|
||||
onChange={(e) => setExpiry(formatExpiry(e.target.value))}
|
||||
placeholder="MM/YY"
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:border-accent-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 text-white placeholder-gray-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
CVV
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={cvv}
|
||||
onChange={(e) => setCvv(e.target.value.replace(/\D/g, '').slice(0, 4))}
|
||||
placeholder="123"
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:border-accent-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 text-white placeholder-gray-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-300 text-sm"
|
||||
>
|
||||
{error}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="w-full py-4 bg-gradient-to-r from-accent-orange to-accent-orange/80 hover:from-accent-orange/90 hover:to-accent-orange/70 rounded-lg font-semibold text-white transition-all glow-orange disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
{processing ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaLock />
|
||||
Complete Purchase - ${album.price}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Security Notice */}
|
||||
<p className="text-xs text-gray-500 text-center mt-4">
|
||||
<FaLock className="inline mr-1" />
|
||||
This is a demo payment form. No real charges will be made.
|
||||
</p>
|
||||
</form>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
'use client';
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { FaCheckCircle, FaTimes, FaDownload } from 'react-icons/fa';
|
||||
import { Album, Purchase } from '@/lib/types';
|
||||
|
||||
interface PurchaseSuccessModalProps {
|
||||
show: boolean;
|
||||
album: Album | null;
|
||||
purchase: Purchase | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function PurchaseSuccessModal({ show, album, purchase, onClose }: PurchaseSuccessModalProps) {
|
||||
if (!album || !purchase) return null;
|
||||
|
||||
const handleDownloadReceipt = () => {
|
||||
// Create a simple text receipt
|
||||
const receipt = `
|
||||
PURCHASE RECEIPT
|
||||
================
|
||||
|
||||
Album: ${album.title}
|
||||
Artist: Parsa
|
||||
Price: $${album.price}
|
||||
Transaction ID: ${purchase.transactionId}
|
||||
Date: ${new Date(purchase.purchaseDate).toLocaleString()}
|
||||
|
||||
Tracks Included:
|
||||
${album.songs.map((song, idx) => `${idx + 1}. ${song.title} (${song.duration})`).join('\n')}
|
||||
|
||||
Thank you for your purchase!
|
||||
You now have full access to this album.
|
||||
`;
|
||||
|
||||
const blob = new Blob([receipt], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `receipt-${purchase.transactionId}.txt`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{show && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||
exit={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="glass-effect rounded-2xl max-w-lg w-full p-8 border-2 border-accent-cyan/30"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-3 bg-green-500/20 rounded-full">
|
||||
<FaCheckCircle className="text-3xl text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">
|
||||
Purchase Successful!
|
||||
</h2>
|
||||
<p className="text-sm text-gray-400">Thank you for your purchase</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-white/10 rounded-full transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<FaTimes className="text-xl text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Receipt Details */}
|
||||
<div className="space-y-4 mb-6">
|
||||
<div className="p-4 bg-white/5 rounded-lg space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Album</span>
|
||||
<span className="text-white font-semibold">{album.title}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Artist</span>
|
||||
<span className="text-white font-semibold">Parsa</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Tracks</span>
|
||||
<span className="text-white font-semibold">{album.songs.length} songs</span>
|
||||
</div>
|
||||
<div className="border-t border-white/10 pt-3 flex justify-between">
|
||||
<span className="text-gray-400">Total Paid</span>
|
||||
<span className="text-accent-orange font-bold text-xl">${album.price}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-accent-cyan/10 border border-accent-cyan/30 rounded-lg">
|
||||
<p className="text-xs text-gray-400 mb-1">Transaction ID</p>
|
||||
<p className="text-sm font-mono text-accent-cyan">{purchase.transactionId}</p>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
{new Date(purchase.purchaseDate).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={handleDownloadReceipt}
|
||||
className="w-full py-3 bg-white/10 hover:bg-white/20 border border-white/20 rounded-lg font-semibold text-white transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<FaDownload />
|
||||
Download Receipt
|
||||
</button>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={onClose}
|
||||
className="w-full py-3 bg-gradient-to-r from-accent-cyan to-accent-cyan/80 hover:from-accent-cyan/90 hover:to-accent-cyan/70 rounded-lg font-semibold text-white transition-all glow-cyan"
|
||||
>
|
||||
Start Listening
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<p className="text-xs text-gray-500 text-center mt-6">
|
||||
You now have unlimited access to all tracks in this album
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user