main: add many things to app :)

Signed-off-by: nfel <nfilsaraee@gmail.com>
This commit is contained in:
2025-12-30 01:30:31 +03:30
parent 91c149e92e
commit 895afc44af
26 changed files with 2180 additions and 249 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ export default function AdminSidebar() {
<h1 className="text-2xl font-bold text-paper-dark border-b-4 border-paper-dark inline-block pb-1">
Admin Panel
</h1>
<p className="text-sm text-paper-gray mt-3">Parsa Music Store</p>
<p className="text-sm text-paper-gray mt-3">Podzahr</p>
</div>
{/* Menu */}
+2 -1
View File
@@ -43,7 +43,8 @@ export default function Biography() {
<h1 className="text-4xl md:text-5xl font-bold text-paper-dark mb-2 tracking-tight border-b-4 border-paper-dark inline-block pb-1">
{artistBio.name}
</h1>
<p className="text-xl text-paper-brown mb-6 font-medium mt-4">{artistBio.title}</p>
<p className="text-lg text-paper-gray mb-2 mt-3">{artistBio.nickname}</p>
<p className="text-xl text-paper-brown mb-6 font-medium">{artistBio.title}</p>
</motion.div>
<motion.div
+5 -3
View File
@@ -5,6 +5,7 @@ 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 { formatPrice } from '@/lib/utils';
import PaymentModal from './PaymentModal';
import PurchaseSuccessModal from './PurchaseSuccessModal';
@@ -37,6 +38,7 @@ export default function CartSidebar({ isOpen, onClose }: CartSidebarProps) {
albumId,
transactionId,
purchaseDate: new Date(),
approvalStatus: 'pending',
};
// Load existing purchases
@@ -45,7 +47,7 @@ export default function CartSidebar({ isOpen, onClose }: CartSidebarProps) {
const updatedPurchases = [...existingPurchases, purchase];
localStorage.setItem('purchases', JSON.stringify(updatedPurchases));
setPurchasedAlbums([...purchasedAlbums, albumId]);
// Don't add to purchasedAlbums yet - wait for approval
setLatestPurchase(purchase);
// Remove from cart
@@ -134,7 +136,7 @@ export default function CartSidebar({ isOpen, onClose }: CartSidebarProps) {
{item.album.year} {item.album.songs.length} tracks
</p>
<p className="text-paper-dark font-bold mt-2">
${item.album.price}
{formatPrice(item.album.price)}
</p>
</div>
@@ -160,7 +162,7 @@ export default function CartSidebar({ isOpen, onClose }: CartSidebarProps) {
<div className="flex items-center justify-between text-xl p-4 paper-card-dark">
<span className="text-paper-light font-semibold">Total</span>
<span className="text-paper-sand font-bold">
${getCartTotal().toFixed(2)}
{formatPrice(getCartTotal())}
</span>
</div>
+2 -2
View File
@@ -38,9 +38,9 @@ export default function Header({ onCartClick }: HeaderProps) {
</div>
<div>
<h1 className="text-xl font-bold text-paper-dark tracking-tight">
Parsa
Podzahr
</h1>
<p className="text-xs text-paper-brown">Progressive Rock</p>
<p className="text-xs text-paper-brown">Parsa Sadatie</p>
</div>
</motion.div>
+27 -27
View File
@@ -125,18 +125,18 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
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-6 md:p-12 pointer-events-none"
className="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none overflow-y-auto"
>
<div className="w-full max-w-sm pointer-events-auto relative">
<div className="w-full max-w-sm pointer-events-auto relative my-auto">
{/* Close Button - Top Right */}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={onClose}
className="absolute -top-2 -right-2 z-10 p-3 bg-paper-light hover:bg-paper-sand border-2 border-paper-dark transition-colors shadow-paper-lg"
className="absolute -top-2 -right-2 z-10 p-2 bg-paper-light hover:bg-paper-sand border-2 border-paper-dark transition-colors shadow-paper-lg"
aria-label="Close player"
>
<FaTimes className="text-xl text-paper-dark" />
<FaTimes className="text-lg text-paper-dark" />
</motion.button>
{/* Album Artwork */}
@@ -144,7 +144,7 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.1 }}
className="relative aspect-square overflow-hidden mb-6 shadow-paper-lg border-4 border-paper-dark"
className="relative aspect-square overflow-hidden mb-3 shadow-paper-lg border-4 border-paper-dark"
>
<div className="absolute inset-0 bg-paper-brown"></div>
<div className="absolute inset-0 cardboard-texture"></div>
@@ -156,8 +156,8 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
{/* Preview Badge */}
{!isPurchased && (
<div className="absolute top-3 right-3 bg-paper-dark text-paper-light px-3 py-1 border-2 border-paper-brown text-xs font-semibold shadow-paper">
Preview Mode
<div className="absolute top-2 right-2 bg-paper-dark text-paper-light px-2 py-1 border-2 border-paper-brown text-xs font-semibold shadow-paper">
Preview
</div>
)}
</motion.div>
@@ -167,15 +167,15 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-center mb-6 p-4 paper-card"
className="text-center mb-3 p-3 paper-card"
>
<h2 className="text-xl md:text-2xl font-bold text-paper-dark mb-1">
<h2 className="text-lg font-bold text-paper-dark mb-0.5">
{currentSong?.title}
</h2>
<p className="text-base text-paper-brown">
<p className="text-sm text-paper-brown">
{album.title}
</p>
<p className="text-xs text-paper-gray mt-1">
<p className="text-xs text-paper-gray mt-0.5">
Track {currentSongIndex + 1} of {album.songs.length}
</p>
</motion.div>
@@ -185,7 +185,7 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mb-5 p-4 paper-card-light"
className="mb-3 p-3 paper-card-light"
>
<div className="relative">
<input
@@ -212,29 +212,29 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="flex items-center justify-center gap-6 mb-6 p-4 paper-card"
className="flex items-center justify-center gap-4 mb-3 p-3 paper-card"
>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handlePrevious}
className="p-3 hover:bg-paper-brown hover:text-paper-light border-2 border-transparent hover:border-paper-dark transition-colors"
className="p-2 hover:bg-paper-brown hover:text-paper-light border-2 border-transparent hover:border-paper-dark transition-colors"
aria-label="Previous track"
>
<FaStepBackward className="text-xl text-paper-dark" />
<FaStepBackward className="text-lg text-paper-dark" />
</motion.button>
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={togglePlay}
className="p-5 bg-paper-dark hover:bg-paper-brown border-2 border-paper-brown transition-all shadow-paper-lg"
className="p-4 bg-paper-dark hover:bg-paper-brown border-2 border-paper-brown transition-all shadow-paper-lg"
aria-label={isPlaying ? 'Pause' : 'Play'}
>
{isPlaying ? (
<FaPause className="text-2xl text-paper-light" />
<FaPause className="text-xl text-paper-light" />
) : (
<FaPlay className="text-2xl text-paper-light ml-0.5" />
<FaPlay className="text-xl text-paper-light ml-0.5" />
)}
</motion.button>
@@ -242,10 +242,10 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleNext}
className="p-3 hover:bg-paper-brown hover:text-paper-light border-2 border-transparent hover:border-paper-dark transition-colors"
className="p-2 hover:bg-paper-brown hover:text-paper-light border-2 border-transparent hover:border-paper-dark transition-colors"
aria-label="Next track"
>
<FaStepForward className="text-xl text-paper-dark" />
<FaStepForward className="text-lg text-paper-dark" />
</motion.button>
</motion.div>
@@ -258,7 +258,7 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => onPurchase(album)}
className="w-full mb-3 py-3 bg-paper-brown hover:bg-paper-dark border-2 border-paper-dark font-semibold text-paper-light text-base transition-all shadow-paper-lg"
className="w-full mb-2 py-2.5 bg-paper-brown hover:bg-paper-dark border-2 border-paper-dark font-semibold text-paper-light text-sm transition-all shadow-paper-lg"
>
Purchase Full Album - {formatPrice(album.price)}
</motion.button>
@@ -272,7 +272,7 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
>
<button
onClick={() => setShowTrackList(!showTrackList)}
className="w-full py-2.5 bg-paper-sand hover:bg-paper-gray border-2 border-paper-brown transition-colors flex items-center justify-center gap-2 text-paper-dark text-sm font-medium"
className="w-full py-2 bg-paper-sand hover:bg-paper-gray border-2 border-paper-brown transition-colors flex items-center justify-center gap-2 text-paper-dark text-xs font-medium"
>
{showTrackList ? <FaChevronUp className="text-xs" /> : <FaChevronDown className="text-xs" />}
<span className="font-medium">
@@ -289,7 +289,7 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
transition={{ duration: 0.3 }}
className="overflow-hidden"
>
<div className="mt-3 max-h-52 overflow-y-auto bg-paper-light border-2 border-paper-brown p-2">
<div className="mt-2 max-h-40 overflow-y-auto bg-paper-light border-2 border-paper-brown p-1.5">
<div className="space-y-0.5">
{album.songs.map((song, index) => (
<button
@@ -299,18 +299,18 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
setIsPlaying(false);
setCurrentTime(0);
}}
className={`w-full text-left px-3 py-2 transition-all border-2 ${
className={`w-full text-left px-2 py-1.5 transition-all border ${
index === currentSongIndex
? 'bg-paper-brown text-paper-light border-paper-dark'
: 'hover:bg-paper-sand text-paper-dark border-transparent'
}`}
>
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<span className="text-xs text-paper-gray font-mono w-5">
<div className="flex items-center gap-1.5">
<span className="text-xs text-paper-gray font-mono w-4">
{String(index + 1).padStart(2, '0')}
</span>
<span className="text-sm font-medium">{song.title}</span>
<span className="text-xs font-medium">{song.title}</span>
</div>
<span className="text-xs text-paper-gray font-mono">{song.duration}</span>
</div>
+161 -74
View File
@@ -2,10 +2,12 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { FaTimes, FaCreditCard, FaLock } from 'react-icons/fa';
import { FaTimes, FaCreditCard, FaLock, FaUniversity } from 'react-icons/fa';
import { Album } from '@/lib/types';
import { formatPrice } from '@/lib/utils';
type PaymentMethod = 'ipg' | 'card-to-card';
interface PaymentModalProps {
album: Album | null;
onClose: () => void;
@@ -13,6 +15,7 @@ interface PaymentModalProps {
}
export default function PaymentModal({ album, onClose, onSuccess }: PaymentModalProps) {
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>('ipg');
const [cardNumber, setCardNumber] = useState('');
const [cardName, setCardName] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');
@@ -28,10 +31,22 @@ export default function PaymentModal({ album, onClose, onSuccess }: PaymentModal
};
const formatPhoneNumber = (value: string) => {
const numbers = value.replace(/\D/g, '');
if (numbers.length <= 3) return numbers;
if (numbers.length <= 6) return `(${numbers.slice(0, 3)}) ${numbers.slice(3)}`;
return `(${numbers.slice(0, 3)}) ${numbers.slice(3, 6)}-${numbers.slice(6, 10)}`;
// Remove all non-digits
let numbers = value.replace(/\D/g, '');
// Remove leading 98 if user types it (we'll add +98 prefix)
if (numbers.startsWith('98')) {
numbers = numbers.slice(2);
}
// Limit to 10 digits (Iranian mobile number length)
numbers = numbers.slice(0, 10);
// Format as +98 XXXX XXX XXXX
if (numbers.length === 0) return '';
if (numbers.length <= 4) return `+98 ${numbers}`;
if (numbers.length <= 7) return `+98 ${numbers.slice(0, 4)} ${numbers.slice(4)}`;
return `+98 ${numbers.slice(0, 4)} ${numbers.slice(4, 7)} ${numbers.slice(7)}`;
};
const handleSubmit = async (e: React.FormEvent) => {
@@ -39,18 +54,17 @@ export default function PaymentModal({ album, onClose, onSuccess }: PaymentModal
setError('');
// Basic validation
if (cardNumber.replace(/\s/g, '').length !== 16) {
setError('Invalid card number');
return;
}
if (!cardName.trim()) {
setError('Name is required');
return;
}
if (phoneNumber.replace(/\D/g, '').length < 10) {
setError('Invalid phone number');
// Validate Iranian phone number (should have 10 digits after +98)
const phoneDigits = phoneNumber.replace(/\D/g, '');
// Remove the country code (98) if present to get local number
const localNumber = phoneDigits.startsWith('98') ? phoneDigits.slice(2) : phoneDigits;
if (localNumber.length !== 10 || !localNumber.startsWith('9')) {
setError('Invalid phone number. Must start with 9 and have 10 digits');
return;
}
@@ -60,9 +74,16 @@ export default function PaymentModal({ album, onClose, onSuccess }: PaymentModal
return;
}
if (!txReceipt.trim()) {
setError('Transaction receipt is required');
return;
// Validate based on payment method
if (paymentMethod === 'card-to-card') {
if (cardNumber.replace(/\s/g, '').length !== 16) {
setError('Invalid card number');
return;
}
if (!txReceipt.trim()) {
setError('Transaction receipt is required');
return;
}
}
if (!album) return;
@@ -70,10 +91,35 @@ export default function PaymentModal({ album, onClose, onSuccess }: PaymentModal
setProcessing(true);
try {
// Use the provided transaction receipt as the ID
// Handle IPG payment
if (paymentMethod === 'ipg') {
const response = await fetch('/api/payment/initiate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
albumId: album.id,
amount: album.price,
customerName: cardName.trim(),
email: email.trim(),
phoneNumber: phoneNumber,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to initiate payment');
}
// Redirect to ZarinPal payment gateway
window.location.href = data.paymentUrl;
return;
}
// Handle card-to-card payment
const transactionId = txReceipt.trim() || 'TXN-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9).toUpperCase();
// Create purchase via API
// Create purchase via API (pending approval for card-to-card)
const response = await fetch('/api/purchases', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -85,6 +131,8 @@ export default function PaymentModal({ album, onClose, onSuccess }: PaymentModal
phoneNumber: phoneNumber,
txReceipt: txReceipt.trim(),
purchaseDate: new Date().getTime(),
approvalStatus: 'pending',
paymentMethod: 'card-to-card',
}),
});
@@ -110,7 +158,7 @@ export default function PaymentModal({ album, onClose, onSuccess }: PaymentModal
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"
className="fixed inset-0 bg-paper-dark/90 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={onClose}
>
<motion.div
@@ -118,53 +166,90 @@ export default function PaymentModal({ album, onClose, onSuccess }: PaymentModal
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"
className="paper-card max-w-md w-full p-6 border-4 border-paper-dark shadow-paper-lg"
>
{/* 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">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold text-paper-dark border-b-4 border-paper-dark inline-block pb-1">
Purchase Album
</h2>
<button
onClick={onClose}
className="p-2 hover:bg-white/10 rounded-full transition-colors"
className="p-2 hover:bg-paper-brown hover:text-paper-light border-2 border-paper-brown transition-colors"
aria-label="Close"
>
<FaTimes className="text-xl text-gray-400" />
<FaTimes className="text-lg text-paper-dark" />
</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">{formatPrice(album.price)}</p>
<div className="mb-4 p-3 bg-paper-light border-2 border-paper-brown">
<h3 className="font-semibold text-paper-dark">{album.title}</h3>
<p className="text-sm text-paper-gray">{album.songs.length} tracks {album.genre}</p>
<p className="text-xl font-bold text-paper-brown mt-1">{formatPrice(album.price)}</p>
</div>
{/* Payment Method Selector */}
<div className="mb-4">
<label className="block text-sm font-medium text-paper-dark mb-2">
Payment Method / روش پرداخت
</label>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => setPaymentMethod('ipg')}
className={`p-3 border-2 transition-all ${
paymentMethod === 'ipg'
? 'bg-paper-brown text-paper-light border-paper-dark shadow-paper'
: 'bg-paper-light text-paper-dark border-paper-brown hover:bg-paper-sand'
}`}
>
<FaUniversity className="text-lg mx-auto mb-1" />
<div className="text-xs font-medium">IPG</div>
<div className="text-xs">درگاه پرداخت</div>
</button>
<button
type="button"
onClick={() => setPaymentMethod('card-to-card')}
className={`p-3 border-2 transition-all ${
paymentMethod === 'card-to-card'
? 'bg-paper-brown text-paper-light border-paper-dark shadow-paper'
: 'bg-paper-light text-paper-dark border-paper-brown hover:bg-paper-sand'
}`}
>
<FaCreditCard className="text-lg mx-auto mb-1" />
<div className="text-xs font-medium">Card to Card</div>
<div className="text-xs">کارت به کارت</div>
</button>
</div>
</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>
<form onSubmit={handleSubmit} className="space-y-3">
{/* Show Card Number only for card-to-card */}
{paymentMethod === 'card-to-card' && (
<div>
<label className="block text-sm font-medium text-paper-dark mb-1">
<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-3 py-2 bg-paper-light border-2 border-paper-brown focus:border-paper-dark focus:outline-none text-paper-dark placeholder-paper-gray shadow-paper text-sm"
required
/>
</div>
)}
{/* Card Name */}
{/* Full Name */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
<label className="block text-sm font-medium text-paper-dark mb-1">
Full Name
</label>
<input
@@ -172,15 +257,15 @@ export default function PaymentModal({ album, onClose, onSuccess }: PaymentModal
value={cardName}
onChange={(e) => setCardName(e.target.value)}
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"
className="w-full px-3 py-2 bg-paper-light border-2 border-paper-brown focus:border-paper-dark focus:outline-none text-paper-dark placeholder-paper-gray shadow-paper text-sm"
required
/>
</div>
{/* Phone Number */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Phone Number
<label className="block text-sm font-medium text-paper-dark mb-1">
Phone Number / شماره تماس
</label>
<input
type="tel"
@@ -189,15 +274,15 @@ export default function PaymentModal({ album, onClose, onSuccess }: PaymentModal
const formatted = formatPhoneNumber(e.target.value);
setPhoneNumber(formatted);
}}
placeholder="(123) 456-7890"
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"
placeholder="+98 9390 084 053"
className="w-full px-3 py-2 bg-paper-light border-2 border-paper-brown focus:border-paper-dark focus:outline-none text-paper-dark placeholder-paper-gray shadow-paper text-sm"
required
/>
</div>
{/* Email */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
<label className="block text-sm font-medium text-paper-dark mb-1">
Email Address
</label>
<input
@@ -205,32 +290,34 @@ export default function PaymentModal({ album, onClose, onSuccess }: PaymentModal
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="john@example.com"
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"
className="w-full px-3 py-2 bg-paper-light border-2 border-paper-brown focus:border-paper-dark focus:outline-none text-paper-dark placeholder-paper-gray shadow-paper text-sm"
required
/>
</div>
{/* Transaction Receipt */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Transaction Receipt / ID
</label>
<input
type="text"
value={txReceipt}
onChange={(e) => setTxReceipt(e.target.value)}
placeholder="TXN-123456789"
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>
{/* Transaction Receipt - only for card-to-card */}
{paymentMethod === 'card-to-card' && (
<div>
<label className="block text-sm font-medium text-paper-dark mb-1">
Transaction Receipt / رسید پرداخت
</label>
<input
type="text"
value={txReceipt}
onChange={(e) => setTxReceipt(e.target.value)}
placeholder="TXN-123456789"
className="w-full px-3 py-2 bg-paper-light border-2 border-paper-brown focus:border-paper-dark focus:outline-none text-paper-dark placeholder-paper-gray shadow-paper text-sm"
required
/>
</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"
className="p-2 bg-red-100 border-2 border-red-400 text-red-700 text-sm"
>
{error}
</motion.div>
@@ -240,23 +327,23 @@ export default function PaymentModal({ album, onClose, onSuccess }: PaymentModal
<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"
className="w-full py-3 bg-paper-brown hover:bg-paper-dark border-2 border-paper-dark font-semibold text-paper-light transition-all shadow-paper-lg disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 text-sm"
>
{processing ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-paper-light"></div>
Processing...
</>
) : (
<>
<FaLock />
Complete Purchase - {formatPrice(album.price)}
{paymentMethod === 'ipg' ? 'Proceed to Payment Gateway' : `Complete Purchase - ${formatPrice(album.price)}`}
</>
)}
</button>
{/* Security Notice */}
<p className="text-xs text-gray-500 text-center mt-4">
<p className="text-xs text-paper-gray text-center mt-2">
<FaLock className="inline mr-1" />
This is a demo payment form. No real charges will be made.
</p>
+35 -32
View File
@@ -18,12 +18,12 @@ export default function PurchaseSuccessModal({ show, album, purchase, onClose }:
const handleDownloadReceipt = () => {
// Create a simple text receipt
const receipt = `
PURCHASE RECEIPT
PURCHASE RECEIPT / رسید خرید
================
Status: PENDING APPROVAL
Status: PENDING APPROVAL / در انتظار تایید
Album: ${album.title}
Artist: Parsa
Artist: Parsa Sadatie (@parsadat)
Amount: ${formatPrice(album.price)}
Transaction ID: ${purchase.transactionId}
Date: ${new Date(purchase.purchaseDate).toLocaleString()}
@@ -31,9 +31,11 @@ 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!
Thank you for your purchase! / از خرید شما متشکریم!
Your order is pending admin approval.
سفارش شما در انتظار تایید مدیر است.
You will receive access to this album after confirmation.
پس از تایید به آلبوم دسترسی خواهید داشت.
`;
const blob = new Blob([receipt], { type: 'text/plain' });
@@ -54,7 +56,7 @@ You will receive access to this album after confirmation.
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"
className="fixed inset-0 bg-paper-dark/90 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={onClose}
>
<motion.div
@@ -62,55 +64,55 @@ You will receive access to this album after confirmation.
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"
className="paper-card max-w-lg w-full p-8 border-4 border-paper-dark shadow-paper-lg"
>
{/* Header */}
<div className="flex items-start justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-3 bg-accent-orange/20 rounded-full">
<FaClock className="text-3xl text-accent-orange" />
<div className="p-3 bg-paper-brown border-2 border-paper-dark">
<FaClock className="text-3xl text-paper-light" />
</div>
<div>
<h2 className="text-2xl font-bold text-white">
<h2 className="text-2xl font-bold text-paper-dark border-b-4 border-paper-dark inline-block pb-1">
Purchase Pending Approval
</h2>
<p className="text-sm text-gray-400">Please wait for admin confirmation</p>
<p className="text-sm text-paper-gray mt-2">در انتظار تایید مدیر</p>
</div>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-white/10 rounded-full transition-colors"
className="p-2 hover:bg-paper-brown hover:text-paper-light border-2 border-paper-brown transition-colors"
aria-label="Close"
>
<FaTimes className="text-xl text-gray-400" />
<FaTimes className="text-xl text-paper-dark" />
</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="p-4 bg-paper-light border-2 border-paper-brown space-y-3">
<div className="flex justify-between">
<span className="text-gray-400">Album</span>
<span className="text-white font-semibold">{album.title}</span>
<span className="text-paper-gray">Album / آلبوم</span>
<span className="text-paper-dark 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>
<span className="text-paper-gray">Artist / هنرمند</span>
<span className="text-paper-dark font-semibold">Parsa Sadatie</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>
<span className="text-paper-gray">Tracks / تعداد آهنگ</span>
<span className="text-paper-dark 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">Amount</span>
<span className="text-accent-orange font-bold text-xl">{formatPrice(album.price)}</span>
<div className="border-t-2 border-paper-brown pt-3 flex justify-between">
<span className="text-paper-gray">Amount / مبلغ</span>
<span className="text-paper-brown font-bold text-xl">{formatPrice(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">
<div className="p-4 bg-paper-sand border-2 border-paper-dark">
<p className="text-xs text-paper-gray mb-1">Transaction ID / شناسه تراکنش</p>
<p className="text-sm font-mono text-paper-dark font-semibold">{purchase.transactionId}</p>
<p className="text-xs text-paper-brown mt-2">
{new Date(purchase.purchaseDate).toLocaleString()}
</p>
</div>
@@ -120,25 +122,26 @@ You will receive access to this album after confirmation.
<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"
className="w-full py-3 bg-paper-light hover:bg-paper-sand border-2 border-paper-brown font-semibold text-paper-dark transition-all shadow-paper flex items-center justify-center gap-2"
>
<FaDownload />
Download Receipt
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"
className="w-full py-3 bg-paper-brown hover:bg-paper-dark border-2 border-paper-dark font-semibold text-paper-light transition-all shadow-paper-lg"
>
OK, Got It
OK, Got It / متوجه شدم
</motion.button>
</div>
{/* Info */}
<p className="text-xs text-gray-500 text-center mt-6">
Your purchase will be reviewed by an admin. You will receive access after approval.
<p className="text-xs text-paper-gray text-center mt-6">
Your purchase will be reviewed by an admin. You will receive access after approval.<br />
سفارش شما توسط مدیر بررسی میشود. پس از تایید به آلبوم دسترسی خواهید داشت.
</p>
</motion.div>
</motion.div>