main: vibe coded app to feel like paper

Signed-off-by: nfel <nfilsaraee@gmail.com>
This commit is contained in:
nfel 2025-12-30 00:00:16 +03:30
parent acbaebb947
commit 91c149e92e
Signed by: nfel
GPG Key ID: DCC0BF3F92B0D45F
27 changed files with 901 additions and 8696 deletions

View File

@ -1,18 +1,17 @@
# Multi-stage build for Next.js application # Multi-stage build for Next.js application with Bun
# Stage 1: Dependencies # Stage 1: Dependencies
FROM node:20-alpine AS deps FROM oven/bun:1-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
# Copy package files # Copy package files
COPY package.json package-lock.json* ./ COPY package.json bun.lockb* ./
# Install dependencies # Install dependencies with Bun
RUN npm ci RUN bun install --frozen-lockfile
# Stage 2: Builder # Stage 2: Builder
FROM node:20-alpine AS builder FROM oven/bun:1-alpine AS builder
WORKDIR /app WORKDIR /app
# Copy dependencies from deps stage # Copy dependencies from deps stage
@ -24,10 +23,10 @@ ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production ENV NODE_ENV=production
# Build the application # Build the application
RUN npm run build RUN bun run build
# Stage 3: Runner # Stage 3: Runner
FROM node:20-alpine AS runner FROM oven/bun:1-alpine AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
@ -42,6 +41,9 @@ COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/.next/static ./.next/static
# Create data directory for SQLite database
RUN mkdir -p /app/data && chown -R nextjs:nodejs /app/data
# Change ownership to nextjs user # Change ownership to nextjs user
RUN chown -R nextjs:nodejs /app RUN chown -R nextjs:nodejs /app
@ -54,5 +56,5 @@ EXPOSE 3000
ENV PORT=3000 ENV PORT=3000
ENV HOSTNAME="0.0.0.0" ENV HOSTNAME="0.0.0.0"
# Start the application # Start the application with Bun
CMD ["node", "server.js"] CMD ["bun", "run", "server.js"]

View File

@ -5,6 +5,7 @@ import { motion } from 'framer-motion';
import { FaEdit, FaTrash, FaPlus } from 'react-icons/fa'; import { FaEdit, FaTrash, FaPlus } from 'react-icons/fa';
import { useAlbums } from '@/lib/AlbumsContext'; import { useAlbums } from '@/lib/AlbumsContext';
import { Album } from '@/lib/types'; import { Album } from '@/lib/types';
import { formatPrice } from '@/lib/utils';
import AdminLayout from '@/components/AdminLayout'; import AdminLayout from '@/components/AdminLayout';
import AddAlbumModal from '@/components/AddAlbumModal'; import AddAlbumModal from '@/components/AddAlbumModal';
import EditAlbumModal from '@/components/EditAlbumModal'; import EditAlbumModal from '@/components/EditAlbumModal';
@ -110,7 +111,7 @@ export default function AdminAlbumsPage() {
</p> </p>
<div className="flex items-center gap-4 text-sm text-gray-400"> <div className="flex items-center gap-4 text-sm text-gray-400">
<span>{album.songs.length} tracks</span> <span>{album.songs.length} tracks</span>
<span className="text-accent-orange font-bold">${album.price}</span> <span className="text-accent-orange font-bold">{formatPrice(album.price)}</span>
</div> </div>
</div> </div>

View File

@ -5,6 +5,7 @@ import { motion } from 'framer-motion';
import { FaMusic, FaShoppingCart, FaDollarSign, FaUsers } from 'react-icons/fa'; import { FaMusic, FaShoppingCart, FaDollarSign, FaUsers } from 'react-icons/fa';
import { useAlbums } from '@/lib/AlbumsContext'; import { useAlbums } from '@/lib/AlbumsContext';
import { Purchase } from '@/lib/types'; import { Purchase } from '@/lib/types';
import { formatPrice } from '@/lib/utils';
import AdminLayout from '@/components/AdminLayout'; import AdminLayout from '@/components/AdminLayout';
export default function AdminDashboard() { export default function AdminDashboard() {
@ -51,29 +52,29 @@ export default function AdminDashboard() {
icon: FaMusic, icon: FaMusic,
label: 'Total Albums', label: 'Total Albums',
value: stats.totalAlbums, value: stats.totalAlbums,
color: 'from-accent-cyan to-accent-cyan/80', color: 'bg-paper-brown',
iconBg: 'bg-accent-cyan/20', iconBg: 'bg-paper-brown/20',
}, },
{ {
icon: FaShoppingCart, icon: FaShoppingCart,
label: 'Total Purchases', label: 'Total Purchases',
value: stats.totalPurchases, value: stats.totalPurchases,
color: 'from-accent-orange to-accent-orange/80', color: 'bg-paper-dark',
iconBg: 'bg-accent-orange/20', iconBg: 'bg-paper-dark/20',
}, },
{ {
icon: FaDollarSign, icon: FaDollarSign,
label: 'Total Revenue', label: 'Total Revenue',
value: `$${stats.totalRevenue.toFixed(2)}`, value: formatPrice(stats.totalRevenue),
color: 'from-green-500 to-green-600', color: 'bg-paper-gray',
iconBg: 'bg-green-500/20', iconBg: 'bg-paper-gray/20',
}, },
{ {
icon: FaUsers, icon: FaUsers,
label: 'Active Users', label: 'Active Users',
value: stats.totalPurchases > 0 ? Math.ceil(stats.totalPurchases / 2) : 0, value: stats.totalPurchases > 0 ? Math.ceil(stats.totalPurchases / 2) : 0,
color: 'from-purple-500 to-purple-600', color: 'bg-paper-brown',
iconBg: 'bg-purple-500/20', iconBg: 'bg-paper-brown/30',
}, },
]; ];
@ -82,8 +83,8 @@ export default function AdminDashboard() {
<div className="p-8"> <div className="p-8">
{/* Header */} {/* Header */}
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Dashboard</h1> <h1 className="text-3xl font-bold text-paper-dark mb-2 border-b-4 border-paper-dark inline-block pb-1">Dashboard</h1>
<p className="text-gray-400">Overview of your music store</p> <p className="text-paper-gray mt-4">Overview of your music store</p>
</div> </div>
{/* Stats Grid */} {/* Stats Grid */}
@ -96,15 +97,15 @@ export default function AdminDashboard() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }} transition={{ delay: index * 0.1 }}
className="glass-effect rounded-xl p-6 border border-white/10" className="paper-card p-6 border-2 border-paper-brown shadow-paper"
> >
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className={`p-3 ${stat.iconBg} rounded-lg`}> <div className={`p-3 ${stat.iconBg} border-2 border-paper-brown`}>
<Icon className="text-2xl text-white" /> <Icon className="text-2xl text-paper-dark" />
</div> </div>
<div> <div>
<p className="text-sm text-gray-400">{stat.label}</p> <p className="text-sm text-paper-gray">{stat.label}</p>
<p className="text-2xl font-bold text-white">{stat.value}</p> <p className="text-2xl font-bold text-paper-dark">{stat.value}</p>
</div> </div>
</div> </div>
</motion.div> </motion.div>
@ -117,9 +118,9 @@ export default function AdminDashboard() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }} transition={{ delay: 0.4 }}
className="glass-effect rounded-xl p-6 border border-white/10" className="paper-card p-6 border-2 border-paper-brown shadow-paper"
> >
<h2 className="text-xl font-bold text-white mb-4">Recent Purchases</h2> <h2 className="text-xl font-bold text-paper-dark mb-4 border-b-2 border-paper-dark pb-2">Recent Purchases</h2>
{stats.recentPurchases.length > 0 ? ( {stats.recentPurchases.length > 0 ? (
<div className="space-y-3"> <div className="space-y-3">
{stats.recentPurchases.map((purchase) => { {stats.recentPurchases.map((purchase) => {
@ -127,25 +128,25 @@ export default function AdminDashboard() {
return ( return (
<div <div
key={purchase.transactionId} key={purchase.transactionId}
className="flex items-center justify-between p-4 bg-white/5 rounded-lg" className="flex items-center justify-between p-4 bg-paper-light border-2 border-paper-brown/30"
> >
<div> <div>
<p className="text-white font-medium">{album?.title || 'Unknown Album'}</p> <p className="text-paper-dark font-medium">{album?.title || 'Unknown Album'}</p>
<p className="text-sm text-gray-400"> <p className="text-sm text-paper-gray">
{new Date(purchase.purchaseDate).toLocaleDateString()} at{' '} {new Date(purchase.purchaseDate).toLocaleDateString()} at{' '}
{new Date(purchase.purchaseDate).toLocaleTimeString()} {new Date(purchase.purchaseDate).toLocaleTimeString()}
</p> </p>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-accent-orange font-bold">${album?.price || 0}</p> <p className="text-paper-brown font-bold">{formatPrice(album?.price || 0)}</p>
<p className="text-xs text-gray-500 font-mono">{purchase.transactionId.slice(0, 12)}...</p> <p className="text-xs text-paper-gray font-mono">{purchase.transactionId.slice(0, 12)}...</p>
</div> </div>
</div> </div>
); );
})} })}
</div> </div>
) : ( ) : (
<p className="text-gray-400 text-center py-8">No purchases yet</p> <p className="text-paper-gray text-center py-8">No purchases yet</p>
)} )}
</motion.div> </motion.div>
</div> </div>

View File

@ -26,28 +26,28 @@ export default function AdminLoginPage() {
}; };
return ( return (
<div className="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-primary-900 via-primary-800 to-primary-900"> <div className="min-h-screen flex items-center justify-center p-4 bg-paper-light">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="w-full max-w-md" className="w-full max-w-md"
> >
<div className="glass-effect rounded-2xl p-8 border border-white/10"> <div className="paper-card p-8 border-2 border-paper-dark shadow-paper-lg">
{/* Header */} {/* Header */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="inline-block p-4 bg-accent-cyan/20 rounded-full mb-4"> <div className="inline-block p-4 bg-paper-brown/20 border-2 border-paper-brown mb-4">
<FaLock className="text-4xl text-accent-cyan" /> <FaLock className="text-4xl text-paper-brown" />
</div> </div>
<h1 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-accent-cyan to-accent-orange mb-2"> <h1 className="text-3xl font-bold text-paper-dark mb-2 border-b-4 border-paper-dark inline-block pb-1">
Admin Panel Admin Panel
</h1> </h1>
<p className="text-gray-400">Sign in to manage your music store</p> <p className="text-paper-gray mt-4">Sign in to manage your music store</p>
</div> </div>
{/* Login Form */} {/* Login Form */}
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<div> <div>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-paper-dark mb-2">
<FaUser className="inline mr-2" /> <FaUser className="inline mr-2" />
Username Username
</label> </label>
@ -56,13 +56,13 @@ export default function AdminLoginPage() {
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
placeholder="admin" placeholder="admin"
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-4 py-3 bg-paper-light border-2 border-paper-brown focus:border-paper-dark focus:outline-none text-paper-dark placeholder-paper-gray shadow-paper"
required required
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-paper-dark mb-2">
<FaLock className="inline mr-2" /> <FaLock className="inline mr-2" />
Password Password
</label> </label>
@ -71,7 +71,7 @@ export default function AdminLoginPage() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••" placeholder="••••••••"
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-4 py-3 bg-paper-light border-2 border-paper-brown focus:border-paper-dark focus:outline-none text-paper-dark placeholder-paper-gray shadow-paper"
required required
/> />
</div> </div>
@ -80,7 +80,7 @@ export default function AdminLoginPage() {
<motion.div <motion.div
initial={{ opacity: 0, y: -10 }} initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }} 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-3 bg-red-100 border-2 border-red-400 text-red-700 text-sm"
> >
{error} {error}
</motion.div> </motion.div>
@ -88,18 +88,18 @@ export default function AdminLoginPage() {
<button <button
type="submit" type="submit"
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"
> >
Sign In Sign In
</button> </button>
</form> </form>
{/* Demo Credentials */} {/* Demo Credentials */}
<div className="mt-6 p-4 bg-white/5 rounded-lg"> <div className="mt-6 p-4 bg-paper-brown/10 border-2 border-paper-brown">
<p className="text-xs text-gray-400 text-center"> <p className="text-xs text-paper-dark text-center">
Demo Credentials:<br /> Demo Credentials:<br />
<span className="text-accent-cyan">Username: admin</span><br /> <span className="text-paper-brown font-semibold">Username: admin</span><br />
<span className="text-accent-cyan">Password: admin123</span> <span className="text-paper-brown font-semibold">Password: admin123</span>
</p> </p>
</div> </div>
</div> </div>

View File

@ -5,6 +5,7 @@ import { motion } from 'framer-motion';
import { FaDownload, FaSearch } from 'react-icons/fa'; import { FaDownload, FaSearch } from 'react-icons/fa';
import { useAlbums } from '@/lib/AlbumsContext'; import { useAlbums } from '@/lib/AlbumsContext';
import { Purchase } from '@/lib/types'; import { Purchase } from '@/lib/types';
import { formatPrice } from '@/lib/utils';
import AdminLayout from '@/components/AdminLayout'; import AdminLayout from '@/components/AdminLayout';
export default function AdminPurchasesPage() { export default function AdminPurchasesPage() {
@ -42,7 +43,7 @@ export default function AdminPurchasesPage() {
date.toLocaleTimeString(), date.toLocaleTimeString(),
purchase.transactionId, purchase.transactionId,
album?.title || 'Unknown', album?.title || 'Unknown',
`$${album?.price || 0}`, formatPrice(album?.price || 0),
]; ];
}); });
@ -64,16 +65,16 @@ export default function AdminPurchasesPage() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<div> <div>
<h1 className="text-3xl font-bold text-white mb-2">Purchase History</h1> <h1 className="text-3xl font-bold text-paper-dark mb-2 border-b-4 border-paper-dark inline-block pb-1">Purchase History</h1>
<p className="text-gray-400"> <p className="text-paper-gray mt-4">
{purchases.length} total purchases ${totalRevenue.toFixed(2)} revenue {purchases.length} total purchases {formatPrice(totalRevenue)} revenue
</p> </p>
</div> </div>
<motion.button <motion.button
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
onClick={exportToCSV} onClick={exportToCSV}
className="px-6 py-3 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 flex items-center gap-2" className="px-6 py-3 bg-paper-brown hover:bg-paper-dark border-2 border-paper-dark font-semibold text-paper-light transition-all shadow-paper-lg flex items-center gap-2"
> >
<FaDownload /> <FaDownload />
Export CSV Export CSV
@ -83,13 +84,13 @@ export default function AdminPurchasesPage() {
{/* Search */} {/* Search */}
<div className="mb-6"> <div className="mb-6">
<div className="relative"> <div className="relative">
<FaSearch className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" /> <FaSearch className="absolute left-4 top-1/2 -translate-y-1/2 text-paper-gray" />
<input <input
type="text" type="text"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search by album name or transaction ID..." placeholder="Search by album name or transaction ID..."
className="w-full pl-12 pr-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 pl-12 pr-4 py-3 bg-paper-light border-2 border-paper-brown focus:border-paper-dark focus:outline-none text-paper-dark placeholder-paper-gray shadow-paper"
/> />
</div> </div>
</div> </div>
@ -98,18 +99,18 @@ export default function AdminPurchasesPage() {
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="glass-effect rounded-xl border border-white/10 overflow-hidden" className="paper-card border-2 border-paper-brown shadow-paper overflow-hidden"
> >
{filteredPurchases.length > 0 ? ( {filteredPurchases.length > 0 ? (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead className="bg-white/5 border-b border-white/10"> <thead className="bg-paper-brown/20 border-b-2 border-paper-brown">
<tr> <tr>
<th className="text-left p-4 text-sm font-semibold text-gray-400">Date & Time</th> <th className="text-left p-4 text-sm font-semibold text-paper-dark">Date & Time</th>
<th className="text-left p-4 text-sm font-semibold text-gray-400">Transaction ID</th> <th className="text-left p-4 text-sm font-semibold text-paper-dark">Transaction ID</th>
<th className="text-left p-4 text-sm font-semibold text-gray-400">Album</th> <th className="text-left p-4 text-sm font-semibold text-paper-dark">Album</th>
<th className="text-left p-4 text-sm font-semibold text-gray-400">Genre</th> <th className="text-left p-4 text-sm font-semibold text-paper-dark">Genre</th>
<th className="text-right p-4 text-sm font-semibold text-gray-400">Price</th> <th className="text-right p-4 text-sm font-semibold text-paper-dark">Price</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -123,24 +124,24 @@ export default function AdminPurchasesPage() {
initial={{ opacity: 0, x: -20 }} initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }} transition={{ delay: index * 0.05 }}
className="border-b border-white/5 hover:bg-white/5 transition-colors" className="border-b border-paper-brown/30 hover:bg-paper-sand/50 transition-colors"
> >
<td className="p-4 text-white"> <td className="p-4 text-paper-dark">
<div className="text-sm">{date.toLocaleDateString()}</div> <div className="text-sm">{date.toLocaleDateString()}</div>
<div className="text-xs text-gray-500">{date.toLocaleTimeString()}</div> <div className="text-xs text-paper-gray">{date.toLocaleTimeString()}</div>
</td> </td>
<td className="p-4"> <td className="p-4">
<code className="text-xs text-accent-cyan bg-accent-cyan/10 px-2 py-1 rounded"> <code className="text-xs text-paper-brown bg-paper-brown/10 px-2 py-1 border border-paper-brown">
{purchase.transactionId} {purchase.transactionId}
</code> </code>
</td> </td>
<td className="p-4"> <td className="p-4">
<div className="text-white font-medium">{album?.title || 'Unknown'}</div> <div className="text-paper-dark font-medium">{album?.title || 'Unknown'}</div>
<div className="text-xs text-gray-500">{album?.songs.length} tracks</div> <div className="text-xs text-paper-gray">{album?.songs.length} tracks</div>
</td> </td>
<td className="p-4 text-gray-400 text-sm">{album?.genre}</td> <td className="p-4 text-paper-gray text-sm">{album?.genre}</td>
<td className="p-4 text-right"> <td className="p-4 text-right">
<span className="text-accent-orange font-bold">${album?.price || 0}</span> <span className="text-paper-brown font-bold">{formatPrice(album?.price || 0)}</span>
</td> </td>
</motion.tr> </motion.tr>
); );
@ -150,7 +151,7 @@ export default function AdminPurchasesPage() {
</div> </div>
) : ( ) : (
<div className="p-12 text-center"> <div className="p-12 text-center">
<p className="text-gray-400"> <p className="text-paper-gray">
{searchTerm ? 'No purchases found matching your search' : 'No purchases yet'} {searchTerm ? 'No purchases found matching your search' : 'No purchases yet'}
</p> </p>
</div> </div>

View File

@ -7,6 +7,7 @@ import { FaPlay, FaShoppingCart, FaArrowLeft, FaClock, FaMusic } from 'react-ico
import { Album, Purchase } from '@/lib/types'; import { Album, Purchase } from '@/lib/types';
import { useCart } from '@/lib/CartContext'; import { useCart } from '@/lib/CartContext';
import { useAlbums } from '@/lib/AlbumsContext'; import { useAlbums } from '@/lib/AlbumsContext';
import { formatPrice } from '@/lib/utils';
import Header from '@/components/Header'; import Header from '@/components/Header';
import CartSidebar from '@/components/CartSidebar'; import CartSidebar from '@/components/CartSidebar';
import MusicPlayer from '@/components/MusicPlayer'; import MusicPlayer from '@/components/MusicPlayer';
@ -78,10 +79,10 @@ export default function AlbumDetailPage() {
return ( return (
<div className="min-h-screen flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<h1 className="text-3xl font-bold text-white mb-4">Album not found</h1> <h1 className="text-3xl font-bold text-paper-dark mb-4">Album not found</h1>
<button <button
onClick={() => router.push('/')} onClick={() => router.push('/')}
className="px-6 py-3 bg-accent-cyan hover:bg-accent-cyan/80 rounded-lg text-white transition-all" className="px-6 py-3 bg-paper-brown hover:bg-paper-dark border-2 border-paper-dark text-paper-light transition-all shadow-paper"
> >
Go Back Home Go Back Home
</button> </button>
@ -112,7 +113,7 @@ export default function AlbumDetailPage() {
<div className="max-w-7xl mx-auto px-4 md:px-8 py-6"> <div className="max-w-7xl mx-auto px-4 md:px-8 py-6">
<button <button
onClick={() => router.push('/')} onClick={() => router.push('/')}
className="flex items-center gap-2 text-gray-400 hover:text-accent-cyan transition-colors" className="flex items-center gap-2 text-paper-dark hover:text-paper-brown transition-colors font-medium border-b-2 border-transparent hover:border-paper-brown"
> >
<FaArrowLeft /> <FaArrowLeft />
Back to Albums Back to Albums
@ -129,15 +130,15 @@ export default function AlbumDetailPage() {
transition={{ duration: 0.6 }} transition={{ duration: 0.6 }}
className="relative" className="relative"
> >
<div className="aspect-square rounded-2xl overflow-hidden bg-gradient-to-br from-primary-600/50 to-primary-800/50 flex items-center justify-center border-2 border-accent-cyan/50 glow-cyan"> <div className="aspect-square overflow-hidden bg-paper-brown flex items-center justify-center border-4 border-paper-dark shadow-paper-lg">
<div className="absolute inset-0 bg-gradient-to-br from-accent-cyan/20 to-accent-orange/20"></div> <div className="absolute inset-0 cardboard-texture"></div>
<div className="relative z-10 text-6xl md:text-8xl font-bold text-white/20 p-8 text-center"> <div className="relative z-10 text-6xl md:text-8xl font-bold text-paper-dark/20 p-8 text-center">
{album.title} {album.title}
</div> </div>
</div> </div>
{isPurchased && ( {isPurchased && (
<div className="absolute top-6 right-6 bg-accent-cyan text-white px-4 py-2 rounded-full text-sm font-semibold"> <div className="absolute top-6 right-6 bg-paper-dark text-paper-light px-4 py-2 border-2 border-paper-brown text-sm font-semibold shadow-paper">
Owned Owned
</div> </div>
)} )}
@ -151,31 +152,31 @@ export default function AlbumDetailPage() {
className="space-y-6" className="space-y-6"
> >
<div> <div>
<p className="text-sm text-gray-400 mb-2">{album.year} {album.genre}</p> <p className="text-sm text-paper-brown mb-2 font-medium">{album.year} {album.genre}</p>
<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-4"> <h1 className="text-4xl md:text-5xl font-bold text-paper-dark mb-4 tracking-tight border-b-4 border-paper-dark inline-block pb-2">
{album.title} {album.title}
</h1> </h1>
<p className="text-xl text-gray-300 leading-relaxed"> <p className="text-xl text-paper-dark leading-relaxed mt-6">
{album.description} {album.description}
</p> </p>
</div> </div>
{/* Album Stats */} {/* Album Stats */}
<div className="flex gap-6 text-gray-400"> <div className="flex gap-6 text-paper-dark p-4 paper-card">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaMusic className="text-accent-cyan" /> <FaMusic className="text-paper-brown" />
<span>{album.songs.length} tracks</span> <span className="font-medium">{album.songs.length} tracks</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaClock className="text-accent-cyan" /> <FaClock className="text-paper-brown" />
<span>{formatTotalDuration(totalDuration)}</span> <span className="font-medium">{formatTotalDuration(totalDuration)}</span>
</div> </div>
</div> </div>
{/* Price */} {/* Price */}
{!isPurchased && ( {!isPurchased && (
<div className="text-3xl font-bold text-accent-orange"> <div className="text-3xl font-bold text-paper-light p-4 paper-card-dark">
${album.price} {formatPrice(album.price)}
</div> </div>
)} )}
@ -183,7 +184,7 @@ export default function AlbumDetailPage() {
<div className="flex gap-4 pt-4"> <div className="flex gap-4 pt-4">
<button <button
onClick={() => setCurrentAlbum(album)} onClick={() => setCurrentAlbum(album)}
className="flex-1 py-4 bg-accent-cyan hover:bg-accent-cyan/80 rounded-lg font-semibold text-white transition-all glow-cyan flex items-center justify-center gap-2" className="flex-1 py-4 bg-paper-sand hover:bg-paper-gray border-2 border-paper-dark font-semibold text-paper-dark transition-all shadow-paper hover:shadow-paper-lg flex items-center justify-center gap-2"
> >
<FaPlay /> <FaPlay />
{isPurchased ? 'Play Album' : 'Preview'} {isPurchased ? 'Play Album' : 'Preview'}
@ -193,7 +194,7 @@ export default function AlbumDetailPage() {
<> <>
<button <button
onClick={() => setAlbumToPurchase(album)} onClick={() => setAlbumToPurchase(album)}
className="flex-1 py-4 bg-accent-orange hover:bg-accent-orange/80 rounded-lg font-semibold text-white transition-all glow-orange flex items-center justify-center gap-2" className="flex-1 py-4 bg-paper-brown hover:bg-paper-dark border-2 border-paper-dark font-semibold text-paper-light transition-all shadow-paper-lg flex items-center justify-center gap-2"
> >
Buy Now Buy Now
</button> </button>
@ -202,9 +203,9 @@ export default function AlbumDetailPage() {
disabled={inCart} disabled={inCart}
className={`py-4 px-6 ${ className={`py-4 px-6 ${
inCart inCart
? 'bg-gray-700 cursor-not-allowed' ? 'bg-paper-gray cursor-not-allowed opacity-50'
: 'bg-white/10 hover:bg-white/20 border border-white/20' : 'bg-paper-light hover:bg-paper-sand border-2 border-paper-brown'
} rounded-lg font-semibold text-white transition-all flex items-center justify-center gap-2`} } font-semibold text-paper-dark transition-all flex items-center justify-center gap-2 shadow-paper`}
> >
<FaShoppingCart /> <FaShoppingCart />
{inCart ? 'In Cart' : 'Add to Cart'} {inCart ? 'In Cart' : 'Add to Cart'}
@ -222,9 +223,9 @@ export default function AlbumDetailPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }} transition={{ duration: 0.6, delay: 0.2 }}
className="glass-effect rounded-2xl p-8" className="paper-card p-8 cardboard-texture"
> >
<h2 className="text-2xl font-bold text-white mb-6">Track List</h2> <h2 className="text-2xl font-bold text-paper-dark mb-6 border-b-2 border-paper-dark pb-2">Track List</h2>
<div className="space-y-2"> <div className="space-y-2">
{album.songs.map((song, index) => ( {album.songs.map((song, index) => (
<motion.div <motion.div
@ -232,29 +233,29 @@ export default function AlbumDetailPage() {
initial={{ opacity: 0, x: -20 }} initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.4, delay: index * 0.05 }} transition={{ duration: 0.4, delay: index * 0.05 }}
className="flex items-center justify-between p-4 rounded-lg hover:bg-white/5 transition-colors group cursor-pointer" className="flex items-center justify-between p-4 hover:bg-paper-sand border-2 border-transparent hover:border-paper-brown transition-all group cursor-pointer"
onClick={() => setCurrentAlbum(album)} onClick={() => setCurrentAlbum(album)}
> >
<div className="flex items-center gap-4 flex-1"> <div className="flex items-center gap-4 flex-1">
<span className="text-gray-500 font-mono text-sm w-8"> <span className="text-paper-gray font-mono text-sm w-8">
{String(index + 1).padStart(2, '0')} {String(index + 1).padStart(2, '0')}
</span> </span>
<div className="flex-1"> <div className="flex-1">
<h3 className="text-white font-medium group-hover:text-accent-cyan transition-colors"> <h3 className="text-paper-dark font-medium group-hover:font-bold transition-all">
{song.title} {song.title}
</h3> </h3>
</div> </div>
</div> </div>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
{!isPurchased && ( {!isPurchased && (
<span className="text-xs text-accent-orange px-2 py-1 bg-accent-orange/20 rounded"> <span className="text-xs text-paper-dark px-2 py-1 bg-paper-brown/20 border border-paper-brown">
Preview Preview
</span> </span>
)} )}
<span className="text-gray-400 text-sm font-mono"> <span className="text-paper-brown text-sm font-mono">
{song.duration} {song.duration}
</span> </span>
<FaPlay className="text-accent-cyan opacity-0 group-hover:opacity-100 transition-opacity" /> <FaPlay className="text-paper-dark opacity-0 group-hover:opacity-100 transition-opacity" />
</div> </div>
</motion.div> </motion.div>
))} ))}

View File

@ -4,20 +4,59 @@
@layer base { @layer base {
body { body {
@apply bg-gradient-to-br from-primary-900 via-primary-800 to-primary-900 text-white min-h-screen; @apply bg-paper-light text-paper-dark min-h-screen;
background-image:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' /%3E%3C/filter%3E%3Crect width='100' height='100' filter='url(%23noise)' opacity='0.05'/%3E%3C/svg%3E");
} }
} }
@layer utilities { @layer utilities {
.glass-effect { .paper-card {
@apply bg-white/10 backdrop-blur-md border border-white/20; @apply bg-paper-sand border-2 border-paper-brown shadow-paper;
} }
.glow-orange { .paper-card-light {
box-shadow: 0 0 20px rgba(255, 107, 53, 0.5); @apply bg-paper-light border-2 border-paper-gray shadow-paper;
} }
.glow-cyan { .paper-card-dark {
box-shadow: 0 0 20px rgba(0, 217, 255, 0.5); @apply bg-paper-brown text-paper-light border-2 border-paper-dark shadow-paper-lg;
}
.cardboard-texture {
background-image:
repeating-linear-gradient(
90deg,
transparent,
transparent 2px,
rgba(47, 41, 38, 0.03) 2px,
rgba(47, 41, 38, 0.03) 4px
),
repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(47, 41, 38, 0.03) 2px,
rgba(47, 41, 38, 0.03) 4px
);
}
.torn-edge {
position: relative;
}
.torn-edge::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 4px;
background-image:
linear-gradient(45deg, transparent 33.33%, currentColor 33.33%, currentColor 66.66%, transparent 66.66%),
linear-gradient(-45deg, transparent 33.33%, currentColor 33.33%, currentColor 66.66%, transparent 66.66%);
background-size: 8px 4px;
background-repeat: repeat-x;
opacity: 0.1;
} }
} }

View File

@ -71,20 +71,15 @@ export default function Home() {
<main className="pt-20"> <main className="pt-20">
{/* Hero Section */} {/* Hero Section */}
<section className="min-h-[60vh] flex items-center justify-center px-4 md:px-8 relative overflow-hidden"> <section className="min-h-[60vh] flex items-center justify-center px-4 md:px-8 relative">
{/* Animated Background */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-accent-cyan/10 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-accent-orange/10 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '1s' }}></div>
</div>
<div className="relative z-10 text-center max-w-4xl mx-auto"> <div className="relative z-10 text-center max-w-4xl mx-auto">
<h1 className="text-5xl md:text-7xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-accent-cyan via-white to-accent-orange mb-6"> <div className="paper-card-dark p-8 md:p-12 cardboard-texture">
<h1 className="text-5xl md:text-7xl font-bold text-paper-light mb-6 tracking-tight">
Progressive Rock Progressive Rock
<br /> <br />
Reimagined Reimagined
</h1> </h1>
<p className="text-xl md:text-2xl text-gray-300 mb-8"> <p className="text-xl md:text-2xl text-paper-sand mb-8">
Explore intricate compositions and sonic landscapes Explore intricate compositions and sonic landscapes
</p> </p>
<button <button
@ -92,11 +87,12 @@ export default function Home() {
const element = document.getElementById('albums'); const element = document.getElementById('albums');
element?.scrollIntoView({ behavior: 'smooth' }); element?.scrollIntoView({ behavior: 'smooth' });
}} }}
className="px-8 py-4 bg-gradient-to-r from-accent-cyan to-accent-orange hover:from-accent-cyan/80 hover:to-accent-orange/80 rounded-full font-semibold text-white transition-all glow-cyan text-lg" className="px-8 py-4 bg-paper-sand border-2 border-paper-dark hover:bg-paper-gray font-semibold text-paper-dark transition-all shadow-paper hover:shadow-paper-lg text-lg"
> >
Explore Albums Explore Albums
</button> </button>
</div> </div>
</div>
</section> </section>
{/* Biography Section */} {/* Biography Section */}
@ -110,12 +106,12 @@ export default function Home() {
/> />
{/* Footer */} {/* Footer */}
<footer className="py-12 px-4 md:px-8 border-t border-white/10 mt-20"> <footer className="py-12 px-4 md:px-8 border-t-2 border-paper-brown mt-20">
<div className="max-w-7xl mx-auto text-center"> <div className="max-w-7xl mx-auto text-center">
<p className="text-gray-400"> <p className="text-paper-dark font-medium">
&copy; {new Date().getFullYear()} Parsa. All rights reserved. &copy; {new Date().getFullYear()} Parsa. All rights reserved.
</p> </p>
<p className="text-sm text-gray-500 mt-2"> <p className="text-sm text-paper-gray mt-2">
Progressive Rock Composer & Producer Progressive Rock Composer & Producer
</p> </p>
</div> </div>

View File

@ -25,9 +25,9 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
} }
return ( return (
<div className="min-h-screen bg-gradient-to-br from-primary-900 via-primary-800 to-primary-900 flex"> <div className="min-h-screen bg-paper-light flex">
<AdminSidebar /> <AdminSidebar />
<main className="flex-1 overflow-auto"> <main className="flex-1 overflow-auto bg-paper-sand/30">
{children} {children}
</main> </main>
</div> </div>

View File

@ -23,13 +23,13 @@ export default function AdminSidebar() {
]; ];
return ( return (
<div className="w-64 bg-primary-900/50 backdrop-blur-sm border-r border-white/10 flex flex-col"> <div className="w-64 bg-paper-sand border-r-2 border-paper-brown flex flex-col">
{/* Header */} {/* Header */}
<div className="p-6 border-b border-white/10"> <div className="p-6 border-b-2 border-paper-brown">
<h1 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-accent-cyan to-accent-orange"> <h1 className="text-2xl font-bold text-paper-dark border-b-4 border-paper-dark inline-block pb-1">
Admin Panel Admin Panel
</h1> </h1>
<p className="text-sm text-gray-400 mt-1">Parsa Music Store</p> <p className="text-sm text-paper-gray mt-3">Parsa Music Store</p>
</div> </div>
{/* Menu */} {/* Menu */}
@ -43,10 +43,10 @@ export default function AdminSidebar() {
key={item.path} key={item.path}
whileHover={{ x: 4 }} whileHover={{ x: 4 }}
onClick={() => router.push(item.path)} onClick={() => router.push(item.path)}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-all ${ className={`w-full flex items-center gap-3 px-4 py-3 border-2 transition-all ${
isActive isActive
? 'bg-accent-cyan/20 text-accent-cyan' ? 'bg-paper-brown text-paper-light border-paper-dark shadow-paper'
: 'text-gray-300 hover:bg-white/5' : 'text-paper-dark border-paper-brown/30 hover:bg-paper-light hover:border-paper-brown'
}`} }`}
> >
<Icon className="text-xl" /> <Icon className="text-xl" />
@ -57,11 +57,11 @@ export default function AdminSidebar() {
</nav> </nav>
{/* Logout */} {/* Logout */}
<div className="p-4 border-t border-white/10"> <div className="p-4 border-t-2 border-paper-brown">
<motion.button <motion.button
whileHover={{ x: 4 }} whileHover={{ x: 4 }}
onClick={handleLogout} onClick={handleLogout}
className="w-full flex items-center gap-3 px-4 py-3 rounded-lg text-red-400 hover:bg-red-500/10 transition-all" className="w-full flex items-center gap-3 px-4 py-3 border-2 border-red-400 text-red-700 hover:bg-red-100 transition-all"
> >
<FaSignOutAlt className="text-xl" /> <FaSignOutAlt className="text-xl" />
<span className="font-medium">Logout</span> <span className="font-medium">Logout</span>

View File

@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
import { FaPlay, FaShoppingCart, FaInfoCircle } from 'react-icons/fa'; import { FaPlay, FaShoppingCart, FaInfoCircle } from 'react-icons/fa';
import { Album } from '@/lib/types'; import { Album } from '@/lib/types';
import { useCart } from '@/lib/CartContext'; import { useCart } from '@/lib/CartContext';
import { formatPrice } from '@/lib/utils';
interface AlbumCardProps { interface AlbumCardProps {
album: Album; album: Album;
@ -41,35 +42,34 @@ export default function AlbumCard({ album, isPurchased, onPlay, onPurchase }: Al
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
viewport={{ once: true }} viewport={{ once: true }}
whileHover={{ y: -10 }} whileHover={{ y: -4, transition: { duration: 0.2 } }}
onClick={handleCardClick} onClick={handleCardClick}
className="glass-effect rounded-xl overflow-hidden group cursor-pointer" className="paper-card-light overflow-hidden group cursor-pointer hover:shadow-paper-lg transition-shadow"
> >
{/* Album Cover */} {/* 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="relative aspect-square bg-paper-brown flex items-center justify-center overflow-hidden border-b-2 border-paper-gray">
<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-paper-dark/20 p-8 text-center">
<div className="relative z-10 text-6xl font-bold text-white/20 p-8 text-center">
{album.title} {album.title}
</div> </div>
{/* Overlay on hover */} {/* 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"> <div className="absolute inset-0 bg-paper-dark/80 opacity-0 group-hover:opacity-100 transition-all duration-300 flex items-center justify-center gap-3">
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onPlay(album); onPlay(album);
}} }}
className="p-4 bg-accent-cyan hover:bg-accent-cyan/80 rounded-full transition-all glow-cyan" className="p-4 bg-paper-sand hover:bg-paper-gray border-2 border-paper-dark transition-all shadow-paper"
aria-label="Play preview" aria-label="Play preview"
> >
<FaPlay className="text-2xl text-white" /> <FaPlay className="text-2xl text-paper-dark" />
</button> </button>
<button <button
onClick={(e) => handleViewDetails(e)} onClick={(e) => handleViewDetails(e)}
className="p-4 bg-white/20 hover:bg-white/30 rounded-full transition-all" className="p-4 bg-paper-light hover:bg-paper-sand border-2 border-paper-brown transition-all shadow-paper"
aria-label="View details" aria-label="View details"
> >
<FaInfoCircle className="text-2xl text-white" /> <FaInfoCircle className="text-2xl text-paper-dark" />
</button> </button>
{!isPurchased && ( {!isPurchased && (
<button <button
@ -77,39 +77,39 @@ export default function AlbumCard({ album, isPurchased, onPlay, onPurchase }: Al
disabled={isInCart(album.id)} disabled={isInCart(album.id)}
className={`p-4 ${ className={`p-4 ${
isInCart(album.id) isInCart(album.id)
? 'bg-gray-700 cursor-not-allowed' ? 'bg-paper-gray border-2 border-paper-brown cursor-not-allowed opacity-50'
: 'bg-accent-orange hover:bg-accent-orange/80 glow-orange' : 'bg-paper-brown hover:bg-paper-dark border-2 border-paper-dark shadow-paper'
} rounded-full transition-all`} } transition-all`}
aria-label="Add to cart" aria-label="Add to cart"
> >
<FaShoppingCart className="text-2xl text-white" /> <FaShoppingCart className="text-2xl text-paper-light" />
</button> </button>
)} )}
</div> </div>
{/* Purchased Badge */} {/* Purchased Badge */}
{isPurchased && ( {isPurchased && (
<div className="absolute top-4 right-4 bg-accent-cyan text-white px-3 py-1 rounded-full text-sm font-semibold"> <div className="absolute top-4 right-4 bg-paper-dark text-paper-light px-3 py-1 border-2 border-paper-brown text-sm font-semibold">
Owned Owned
</div> </div>
)} )}
</div> </div>
{/* Album Info */} {/* Album Info */}
<div className="p-6 space-y-3"> <div className="p-6 space-y-3 bg-paper-light cardboard-texture">
<div> <div>
<h3 className="text-xl font-bold text-white group-hover:text-accent-cyan transition-colors"> <h3 className="text-xl font-bold text-paper-dark group-hover:border-b-2 group-hover:border-paper-dark transition-all inline-block">
{album.title} {album.title}
</h3> </h3>
<p className="text-sm text-gray-400">{album.year} {album.genre}</p> <p className="text-sm text-paper-gray font-medium mt-1">{album.year} {album.genre}</p>
</div> </div>
<p className="text-gray-300 text-sm line-clamp-2">{album.description}</p> <p className="text-paper-dark text-sm line-clamp-2">{album.description}</p>
<div className="flex items-center justify-between pt-2"> <div className="flex items-center justify-between pt-2 border-t-2 border-paper-gray">
<span className="text-sm text-gray-400">{album.songs.length} tracks</span> <span className="text-sm text-paper-brown font-medium">{album.songs.length} tracks</span>
{!isPurchased && ( {!isPurchased && (
<span className="text-lg font-bold text-accent-orange">${album.price}</span> <span className="text-lg font-bold text-paper-dark">{formatPrice(album.price)}</span>
)} )}
</div> </div>
</div> </div>

View File

@ -23,10 +23,10 @@ export default function AlbumShowcase({ purchasedAlbums, onPlay, onPurchase }: A
viewport={{ once: true }} viewport={{ once: true }}
className="text-center mb-16" 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"> <h2 className="text-4xl md:text-5xl font-bold text-paper-dark mb-4 tracking-tight border-b-4 border-paper-dark inline-block pb-2">
Discography Discography
</h2> </h2>
<p className="text-xl text-gray-300 max-w-2xl mx-auto"> <p className="text-xl text-paper-brown max-w-2xl mx-auto mt-6">
Explore a collection of progressive rock albums that push the boundaries of sound and creativity Explore a collection of progressive rock albums that push the boundaries of sound and creativity
</p> </p>
</motion.div> </motion.div>

View File

@ -14,7 +14,7 @@ export default function Biography() {
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }} transition={{ duration: 0.6 }}
viewport={{ once: true }} viewport={{ once: true }}
className="glass-effect rounded-2xl p-8 md:p-12" className="paper-card p-8 md:p-12 cardboard-texture"
> >
<div className="grid md:grid-cols-2 gap-12 items-center"> <div className="grid md:grid-cols-2 gap-12 items-center">
{/* Image Section */} {/* Image Section */}
@ -25,8 +25,8 @@ export default function Biography() {
viewport={{ once: true }} viewport={{ once: true }}
className="relative" 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="aspect-square bg-paper-brown flex items-center justify-center border-4 border-paper-dark shadow-paper-lg">
<div className="text-6xl md:text-8xl font-bold text-accent-cyan/30"> <div className="text-6xl md:text-8xl font-bold text-paper-dark/20">
{artistBio.name} {artistBio.name}
</div> </div>
</div> </div>
@ -40,10 +40,10 @@ export default function Biography() {
transition={{ duration: 0.6, delay: 0.3 }} transition={{ duration: 0.6, delay: 0.3 }}
viewport={{ once: true }} 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"> <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} {artistBio.name}
</h1> </h1>
<p className="text-xl text-accent-cyan/80 mb-6">{artistBio.title}</p> <p className="text-xl text-paper-brown mb-6 font-medium mt-4">{artistBio.title}</p>
</motion.div> </motion.div>
<motion.div <motion.div
@ -51,7 +51,7 @@ export default function Biography() {
whileInView={{ opacity: 1, x: 0 }} whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.4 }} transition={{ duration: 0.6, delay: 0.4 }}
viewport={{ once: true }} viewport={{ once: true }}
className="space-y-4 text-gray-300 leading-relaxed" className="space-y-4 text-paper-dark leading-relaxed"
> >
{artistBio.bio.split('\n\n').map((paragraph, idx) => ( {artistBio.bio.split('\n\n').map((paragraph, idx) => (
<p key={idx}>{paragraph}</p> <p key={idx}>{paragraph}</p>
@ -68,31 +68,31 @@ export default function Biography() {
> >
<a <a
href={artistBio.socialLinks.spotify} href={artistBio.socialLinks.spotify}
className="p-3 glass-effect rounded-lg hover:bg-accent-cyan/20 transition-all hover:glow-cyan" className="p-3 bg-paper-light border-2 border-paper-brown hover:bg-paper-brown hover:border-paper-dark transition-all shadow-paper"
aria-label="Spotify" aria-label="Spotify"
> >
<FaSpotify className="text-2xl text-accent-cyan" /> <FaSpotify className="text-2xl text-paper-dark" />
</a> </a>
<a <a
href={artistBio.socialLinks.youtube} href={artistBio.socialLinks.youtube}
className="p-3 glass-effect rounded-lg hover:bg-accent-orange/20 transition-all hover:glow-orange" className="p-3 bg-paper-light border-2 border-paper-brown hover:bg-paper-brown hover:border-paper-dark transition-all shadow-paper"
aria-label="YouTube" aria-label="YouTube"
> >
<FaYoutube className="text-2xl text-accent-orange" /> <FaYoutube className="text-2xl text-paper-dark" />
</a> </a>
<a <a
href={artistBio.socialLinks.instagram} href={artistBio.socialLinks.instagram}
className="p-3 glass-effect rounded-lg hover:bg-accent-cyan/20 transition-all hover:glow-cyan" className="p-3 bg-paper-light border-2 border-paper-brown hover:bg-paper-brown hover:border-paper-dark transition-all shadow-paper"
aria-label="Instagram" aria-label="Instagram"
> >
<FaInstagram className="text-2xl text-accent-cyan" /> <FaInstagram className="text-2xl text-paper-dark" />
</a> </a>
<a <a
href="#" href="#"
className="p-3 glass-effect rounded-lg hover:bg-accent-orange/20 transition-all hover:glow-orange" className="p-3 bg-paper-light border-2 border-paper-brown hover:bg-paper-brown hover:border-paper-dark transition-all shadow-paper"
aria-label="Apple Music" aria-label="Apple Music"
> >
<SiApplemusic className="text-2xl text-accent-orange" /> <SiApplemusic className="text-2xl text-paper-dark" />
</a> </a>
</motion.div> </motion.div>
</div> </div>

View File

@ -71,7 +71,7 @@ export default function CartSidebar({ isOpen, onClose }: CartSidebarProps) {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
onClick={onClose} onClick={onClose}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40" className="fixed inset-0 bg-paper-dark/80 z-40"
/> />
{/* Sidebar */} {/* Sidebar */}
@ -80,22 +80,22 @@ export default function CartSidebar({ isOpen, onClose }: CartSidebarProps) {
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: '100%' }} exit={{ x: '100%' }}
transition={{ type: 'spring', damping: 30, stiffness: 300 }} 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" className="fixed right-0 top-0 bottom-0 w-full md:w-96 bg-paper-sand border-l-4 border-paper-dark z-50 flex flex-col cardboard-texture"
> >
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-6 border-b border-white/10"> <div className="flex items-center justify-between p-6 border-b-2 border-paper-dark">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<FaShoppingCart className="text-2xl text-accent-cyan" /> <FaShoppingCart className="text-2xl text-paper-dark" />
<h2 className="text-2xl font-bold text-white"> <h2 className="text-2xl font-bold text-paper-dark">
Cart ({cartItems.length}) Cart ({cartItems.length})
</h2> </h2>
</div> </div>
<button <button
onClick={onClose} 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-transparent hover:border-paper-dark transition-colors"
aria-label="Close cart" aria-label="Close cart"
> >
<FaTimes className="text-xl text-gray-400" /> <FaTimes className="text-xl text-paper-dark" />
</button> </button>
</div> </div>
@ -103,9 +103,9 @@ export default function CartSidebar({ isOpen, onClose }: CartSidebarProps) {
<div className="flex-1 overflow-y-auto p-6"> <div className="flex-1 overflow-y-auto p-6">
{cartItems.length === 0 ? ( {cartItems.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center"> <div className="flex flex-col items-center justify-center h-full text-center">
<FaShoppingCart className="text-6xl text-gray-600 mb-4" /> <FaShoppingCart className="text-6xl text-paper-gray mb-4" />
<p className="text-gray-400 text-lg">Your cart is empty</p> <p className="text-paper-dark text-lg font-medium">Your cart is empty</p>
<p className="text-gray-500 text-sm mt-2">Add some albums to get started</p> <p className="text-paper-brown text-sm mt-2">Add some albums to get started</p>
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
@ -115,25 +115,25 @@ export default function CartSidebar({ isOpen, onClose }: CartSidebarProps) {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }} exit={{ opacity: 0, y: -20 }}
className="glass-effect rounded-xl p-4" className="paper-card-light p-4"
> >
<div className="flex gap-4"> <div className="flex gap-4">
{/* Album Cover */} {/* 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"> <div className="w-20 h-20 bg-paper-brown border-2 border-paper-dark flex items-center justify-center flex-shrink-0">
<span className="text-xs text-white/50 font-bold text-center px-2"> <span className="text-xs text-paper-dark/50 font-bold text-center px-2">
{item.album.title} {item.album.title}
</span> </span>
</div> </div>
{/* Album Info */} {/* Album Info */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="text-white font-semibold truncate"> <h3 className="text-paper-dark font-semibold truncate">
{item.album.title} {item.album.title}
</h3> </h3>
<p className="text-sm text-gray-400"> <p className="text-sm text-paper-brown">
{item.album.year} {item.album.songs.length} tracks {item.album.year} {item.album.songs.length} tracks
</p> </p>
<p className="text-accent-orange font-bold mt-2"> <p className="text-paper-dark font-bold mt-2">
${item.album.price} ${item.album.price}
</p> </p>
</div> </div>
@ -141,10 +141,10 @@ export default function CartSidebar({ isOpen, onClose }: CartSidebarProps) {
{/* Remove Button */} {/* Remove Button */}
<button <button
onClick={() => removeFromCart(item.album.id)} onClick={() => removeFromCart(item.album.id)}
className="p-2 hover:bg-red-500/20 rounded-lg transition-colors self-start" className="p-2 hover:bg-paper-brown hover:text-paper-light border-2 border-transparent hover:border-paper-dark transition-colors self-start"
aria-label="Remove from cart" aria-label="Remove from cart"
> >
<FaTrash className="text-red-400" /> <FaTrash className="text-paper-dark" />
</button> </button>
</div> </div>
</motion.div> </motion.div>
@ -155,11 +155,11 @@ export default function CartSidebar({ isOpen, onClose }: CartSidebarProps) {
{/* Footer */} {/* Footer */}
{cartItems.length > 0 && ( {cartItems.length > 0 && (
<div className="border-t border-white/10 p-6 space-y-4"> <div className="border-t-2 border-paper-dark p-6 space-y-4">
{/* Total */} {/* Total */}
<div className="flex items-center justify-between text-xl"> <div className="flex items-center justify-between text-xl p-4 paper-card-dark">
<span className="text-gray-300 font-semibold">Total</span> <span className="text-paper-light font-semibold">Total</span>
<span className="text-accent-orange font-bold"> <span className="text-paper-sand font-bold">
${getCartTotal().toFixed(2)} ${getCartTotal().toFixed(2)}
</span> </span>
</div> </div>
@ -168,13 +168,13 @@ export default function CartSidebar({ isOpen, onClose }: CartSidebarProps) {
<div className="space-y-3"> <div className="space-y-3">
<button <button
onClick={handleCheckout} 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" className="w-full py-4 bg-paper-brown hover:bg-paper-dark border-2 border-paper-dark font-semibold text-paper-light transition-all shadow-paper-lg"
> >
Proceed to Checkout Proceed to Checkout
</button> </button>
<button <button
onClick={clearCart} 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" 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"
> >
Clear Cart Clear Cart
</button> </button>

View File

@ -23,24 +23,24 @@ export default function Header({ onCartClick }: HeaderProps) {
<motion.header <motion.header
initial={{ y: -100 }} initial={{ y: -100 }}
animate={{ y: 0 }} animate={{ y: 0 }}
className="fixed top-0 left-0 right-0 z-40 glass-effect border-b border-white/10" className="fixed top-0 left-0 right-0 z-40 bg-paper-sand border-b-4 border-paper-dark shadow-paper-lg"
> >
<div className="max-w-7xl mx-auto px-4 md:px-8 py-4"> <div className="max-w-7xl mx-auto px-4 md:px-8 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{/* Logo */} {/* Logo */}
<motion.div <motion.div
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.02 }}
className="flex items-center gap-3 cursor-pointer" className="flex items-center gap-3 cursor-pointer"
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} 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"> <div className="p-2 bg-paper-dark border-2 border-paper-brown">
<FaMusic className="text-2xl text-white" /> <FaMusic className="text-2xl text-paper-light" />
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-accent-cyan to-accent-orange"> <h1 className="text-xl font-bold text-paper-dark tracking-tight">
Parsa Parsa
</h1> </h1>
<p className="text-xs text-gray-400">Progressive Rock</p> <p className="text-xs text-paper-brown">Progressive Rock</p>
</div> </div>
</motion.div> </motion.div>
@ -49,13 +49,13 @@ export default function Header({ onCartClick }: HeaderProps) {
<div className="hidden md:flex items-center gap-8"> <div className="hidden md:flex items-center gap-8">
<button <button
onClick={() => scrollToSection('bio')} onClick={() => scrollToSection('bio')}
className="text-gray-300 hover:text-accent-cyan transition-colors" className="text-paper-dark hover:text-paper-brown transition-colors font-medium border-b-2 border-transparent hover:border-paper-brown"
> >
Bio Bio
</button> </button>
<button <button
onClick={() => scrollToSection('albums')} onClick={() => scrollToSection('albums')}
className="text-gray-300 hover:text-accent-cyan transition-colors" className="text-paper-dark hover:text-paper-brown transition-colors font-medium border-b-2 border-transparent hover:border-paper-brown"
> >
Albums Albums
</button> </button>
@ -63,18 +63,18 @@ export default function Header({ onCartClick }: HeaderProps) {
{/* Cart Button */} {/* Cart Button */}
<motion.button <motion.button
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.98 }}
onClick={onCartClick} onClick={onCartClick}
className="relative p-3 bg-white/10 hover:bg-white/20 rounded-lg transition-all" className="relative p-3 bg-paper-brown hover:bg-paper-dark border-2 border-paper-dark transition-all shadow-paper"
aria-label="Shopping cart" aria-label="Shopping cart"
> >
<FaShoppingCart className="text-xl text-accent-cyan" /> <FaShoppingCart className="text-xl text-paper-light" />
{cartCount > 0 && ( {cartCount > 0 && (
<motion.div <motion.div
initial={{ scale: 0 }} initial={{ scale: 0 }}
animate={{ scale: 1 }} 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" className="absolute -top-2 -right-2 bg-paper-dark text-paper-light text-xs font-bold border-2 border-paper-brown w-6 h-6 flex items-center justify-center"
> >
{cartCount} {cartCount}
</motion.div> </motion.div>

View File

@ -4,6 +4,7 @@ import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { FaPlay, FaPause, FaStepBackward, FaStepForward, FaTimes, FaChevronDown, FaChevronUp } from 'react-icons/fa'; import { FaPlay, FaPause, FaStepBackward, FaStepForward, FaTimes, FaChevronDown, FaChevronUp } from 'react-icons/fa';
import { Album, Song } from '@/lib/types'; import { Album, Song } from '@/lib/types';
import { formatPrice } from '@/lib/utils';
interface MusicPlayerProps { interface MusicPlayerProps {
album: Album | null; album: Album | null;
@ -115,7 +116,7 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
onClick={onClose} onClick={onClose}
className="fixed inset-0 bg-black/90 backdrop-blur-xl z-50" className="fixed inset-0 bg-paper-dark/90 z-50"
/> />
{/* Player Modal */} {/* Player Modal */}
@ -129,13 +130,13 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
<div className="w-full max-w-sm pointer-events-auto relative"> <div className="w-full max-w-sm pointer-events-auto relative">
{/* Close Button - Top Right */} {/* Close Button - Top Right */}
<motion.button <motion.button
whileHover={{ scale: 1.1 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.95 }}
onClick={onClose} onClick={onClose}
className="absolute -top-2 -right-2 z-10 p-3 bg-white/90 hover:bg-white rounded-full transition-colors shadow-xl" 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"
aria-label="Close player" aria-label="Close player"
> >
<FaTimes className="text-xl text-primary-900" /> <FaTimes className="text-xl text-paper-dark" />
</motion.button> </motion.button>
{/* Album Artwork */} {/* Album Artwork */}
@ -143,19 +144,19 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
initial={{ scale: 0.8, opacity: 0 }} initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }} animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.1 }} transition={{ delay: 0.1 }}
className="relative aspect-square rounded-2xl overflow-hidden mb-6 shadow-2xl" className="relative aspect-square overflow-hidden mb-6 shadow-paper-lg border-4 border-paper-dark"
> >
<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-paper-brown"></div>
<div className="absolute inset-0 bg-gradient-to-br from-accent-cyan/30 to-accent-orange/30"></div> <div className="absolute inset-0 cardboard-texture"></div>
<div className="absolute inset-0 flex items-center justify-center"> <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"> <div className="text-4xl md:text-5xl font-bold text-paper-dark/30 text-center px-6">
{album.title} {album.title}
</div> </div>
</div> </div>
{/* Preview Badge */} {/* Preview Badge */}
{!isPurchased && ( {!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"> <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 Preview Mode
</div> </div>
)} )}
@ -166,15 +167,15 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }} transition={{ delay: 0.2 }}
className="text-center mb-6" className="text-center mb-6 p-4 paper-card"
> >
<h2 className="text-xl md:text-2xl font-bold text-white mb-1"> <h2 className="text-xl md:text-2xl font-bold text-paper-dark mb-1">
{currentSong?.title} {currentSong?.title}
</h2> </h2>
<p className="text-base text-gray-400"> <p className="text-base text-paper-brown">
{album.title} {album.title}
</p> </p>
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-paper-gray mt-1">
Track {currentSongIndex + 1} of {album.songs.length} Track {currentSongIndex + 1} of {album.songs.length}
</p> </p>
</motion.div> </motion.div>
@ -184,7 +185,7 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }} transition={{ delay: 0.3 }}
className="mb-5" className="mb-5 p-4 paper-card-light"
> >
<div className="relative"> <div className="relative">
<input <input
@ -194,13 +195,13 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
value={currentTime} value={currentTime}
onChange={handleSeek} onChange={handleSeek}
disabled={!isPurchased} disabled={!isPurchased}
className="w-full h-1 bg-gray-700/50 rounded-full appearance-none cursor-pointer" className="w-full h-2 bg-paper-gray appearance-none cursor-pointer border-2 border-paper-brown"
style={{ style={{
background: `linear-gradient(to right, #00d9ff ${(currentTime / (duration || 1)) * 100}%, rgba(255,255,255,0.1) ${(currentTime / (duration || 1)) * 100}%)`, background: `linear-gradient(to right, #5A554C ${(currentTime / (duration || 1)) * 100}%, #D9DACA ${(currentTime / (duration || 1)) * 100}%)`,
}} }}
/> />
</div> </div>
<div className="flex justify-between text-xs text-gray-400 mt-1.5 font-mono"> <div className="flex justify-between text-xs text-paper-dark mt-1.5 font-mono">
<span>{formatTime(currentTime)}</span> <span>{formatTime(currentTime)}</span>
<span>{isPurchased ? formatTime(duration) : '0:30'}</span> <span>{isPurchased ? formatTime(duration) : '0:30'}</span>
</div> </div>
@ -211,40 +212,40 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }} transition={{ delay: 0.4 }}
className="flex items-center justify-center gap-6 mb-6" className="flex items-center justify-center gap-6 mb-6 p-4 paper-card"
> >
<motion.button <motion.button
whileHover={{ scale: 1.1 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.95 }}
onClick={handlePrevious} onClick={handlePrevious}
className="p-3 hover:bg-white/10 rounded-full transition-colors" className="p-3 hover:bg-paper-brown hover:text-paper-light border-2 border-transparent hover:border-paper-dark transition-colors"
aria-label="Previous track" aria-label="Previous track"
> >
<FaStepBackward className="text-xl text-white" /> <FaStepBackward className="text-xl 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"
aria-label={isPlaying ? 'Pause' : 'Play'}
>
{isPlaying ? (
<FaPause className="text-2xl text-paper-light" />
) : (
<FaPlay className="text-2xl text-paper-light ml-0.5" />
)}
</motion.button> </motion.button>
<motion.button <motion.button
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} 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} onClick={handleNext}
className="p-3 hover:bg-white/10 rounded-full transition-colors" className="p-3 hover:bg-paper-brown hover:text-paper-light border-2 border-transparent hover:border-paper-dark transition-colors"
aria-label="Next track" aria-label="Next track"
> >
<FaStepForward className="text-xl text-white" /> <FaStepForward className="text-xl text-paper-dark" />
</motion.button> </motion.button>
</motion.div> </motion.div>
@ -257,9 +258,9 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
onClick={() => onPurchase(album)} 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" 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"
> >
Purchase Full Album - ${album.price} Purchase Full Album - {formatPrice(album.price)}
</motion.button> </motion.button>
)} )}
@ -271,7 +272,7 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
> >
<button <button
onClick={() => setShowTrackList(!showTrackList)} 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" 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"
> >
{showTrackList ? <FaChevronUp className="text-xs" /> : <FaChevronDown className="text-xs" />} {showTrackList ? <FaChevronUp className="text-xs" /> : <FaChevronDown className="text-xs" />}
<span className="font-medium"> <span className="font-medium">
@ -288,7 +289,7 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className="overflow-hidden" className="overflow-hidden"
> >
<div className="mt-3 max-h-52 overflow-y-auto rounded-lg bg-white/5 p-2"> <div className="mt-3 max-h-52 overflow-y-auto bg-paper-light border-2 border-paper-brown p-2">
<div className="space-y-0.5"> <div className="space-y-0.5">
{album.songs.map((song, index) => ( {album.songs.map((song, index) => (
<button <button
@ -298,20 +299,20 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
setIsPlaying(false); setIsPlaying(false);
setCurrentTime(0); setCurrentTime(0);
}} }}
className={`w-full text-left px-3 py-2 rounded-md transition-all ${ className={`w-full text-left px-3 py-2 transition-all border-2 ${
index === currentSongIndex index === currentSongIndex
? 'bg-accent-cyan/20 text-accent-cyan' ? 'bg-paper-brown text-paper-light border-paper-dark'
: 'hover:bg-white/5 text-gray-300' : 'hover:bg-paper-sand text-paper-dark border-transparent'
}`} }`}
> >
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs text-gray-500 font-mono w-5"> <span className="text-xs text-paper-gray font-mono w-5">
{String(index + 1).padStart(2, '0')} {String(index + 1).padStart(2, '0')}
</span> </span>
<span className="text-sm font-medium">{song.title}</span> <span className="text-sm font-medium">{song.title}</span>
</div> </div>
<span className="text-xs text-gray-500 font-mono">{song.duration}</span> <span className="text-xs text-paper-gray font-mono">{song.duration}</span>
</div> </div>
</button> </button>
))} ))}

View File

@ -4,6 +4,7 @@ import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { FaTimes, FaCreditCard, FaLock } from 'react-icons/fa'; import { FaTimes, FaCreditCard, FaLock } from 'react-icons/fa';
import { Album } from '@/lib/types'; import { Album } from '@/lib/types';
import { formatPrice } from '@/lib/utils';
interface PaymentModalProps { interface PaymentModalProps {
album: Album | null; album: Album | null;
@ -137,7 +138,7 @@ export default function PaymentModal({ album, onClose, onSuccess }: PaymentModal
<div className="mb-6 p-4 bg-white/5 rounded-lg"> <div className="mb-6 p-4 bg-white/5 rounded-lg">
<h3 className="font-semibold text-white">{album.title}</h3> <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-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> <p className="text-2xl font-bold text-accent-orange mt-2">{formatPrice(album.price)}</p>
</div> </div>
{/* Payment Form */} {/* Payment Form */}
@ -249,7 +250,7 @@ export default function PaymentModal({ album, onClose, onSuccess }: PaymentModal
) : ( ) : (
<> <>
<FaLock /> <FaLock />
Complete Purchase - ${album.price} Complete Purchase - {formatPrice(album.price)}
</> </>
)} )}
</button> </button>

View File

@ -3,6 +3,7 @@
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { FaClock, FaTimes, FaDownload } from 'react-icons/fa'; import { FaClock, FaTimes, FaDownload } from 'react-icons/fa';
import { Album, Purchase } from '@/lib/types'; import { Album, Purchase } from '@/lib/types';
import { formatPrice } from '@/lib/utils';
interface PurchaseSuccessModalProps { interface PurchaseSuccessModalProps {
show: boolean; show: boolean;
@ -23,7 +24,7 @@ PURCHASE RECEIPT
Status: PENDING APPROVAL Status: PENDING APPROVAL
Album: ${album.title} Album: ${album.title}
Artist: Parsa Artist: Parsa
Amount: $${album.price} Amount: ${formatPrice(album.price)}
Transaction ID: ${purchase.transactionId} Transaction ID: ${purchase.transactionId}
Date: ${new Date(purchase.purchaseDate).toLocaleString()} Date: ${new Date(purchase.purchaseDate).toLocaleString()}
@ -102,7 +103,7 @@ You will receive access to this album after confirmation.
</div> </div>
<div className="border-t border-white/10 pt-3 flex justify-between"> <div className="border-t border-white/10 pt-3 flex justify-between">
<span className="text-gray-400">Amount</span> <span className="text-gray-400">Amount</span>
<span className="text-accent-orange font-bold text-xl">${album.price}</span> <span className="text-accent-orange font-bold text-xl">{formatPrice(album.price)}</span>
</div> </div>
</div> </div>

View File

@ -22,7 +22,10 @@ export const albums: Album[] = [
coverImage: "/albums/echoes-of-time.jpg", coverImage: "/albums/echoes-of-time.jpg",
year: 2024, year: 2024,
genre: "Progressive Rock", genre: "Progressive Rock",
price: 12.99, price: 350000,
tag: "Album",
format: "flac",
bitrate: "lossless",
description: "An epic journey through time and space, featuring complex polyrhythms, atmospheric keyboards, and powerful guitar solos. This concept album tells the story of humanity's relationship with time.", description: "An epic journey through time and space, featuring complex polyrhythms, atmospheric keyboards, and powerful guitar solos. This concept album tells the story of humanity's relationship with time.",
songs: [ songs: [
{ {
@ -68,7 +71,10 @@ export const albums: Album[] = [
coverImage: "/albums/crimson-horizons.jpg", coverImage: "/albums/crimson-horizons.jpg",
year: 2023, year: 2023,
genre: "Progressive Rock", genre: "Progressive Rock",
price: 10.99, price: 450000,
tag: "Deluxe",
format: "mp3",
bitrate: "320kbps",
description: "A darker, heavier exploration of prog rock with crushing riffs and intricate instrumental passages. Features extended improvisational sections and powerful vocals.", description: "A darker, heavier exploration of prog rock with crushing riffs and intricate instrumental passages. Features extended improvisational sections and powerful vocals.",
songs: [ songs: [
{ {
@ -107,7 +113,10 @@ export const albums: Album[] = [
coverImage: "/albums/synthetic-dreams.jpg", coverImage: "/albums/synthetic-dreams.jpg",
year: 2022, year: 2022,
genre: "Progressive Rock / Electronic", genre: "Progressive Rock / Electronic",
price: 9.99, price: 150000,
tag: "EP",
format: "wav",
bitrate: "lossless",
description: "A fusion of progressive rock and electronic elements, exploring the intersection of organic and synthetic sounds. Features vintage synthesizers and modern production.", description: "A fusion of progressive rock and electronic elements, exploring the intersection of organic and synthetic sounds. Features vintage synthesizers and modern production.",
songs: [ songs: [
{ {

View File

@ -1,4 +1,4 @@
import Database from 'better-sqlite3'; import { Database } from 'bun:sqlite';
import path from 'path'; import path from 'path';
import { albums as initialAlbums } from './data'; import { albums as initialAlbums } from './data';
import { Album, Purchase } from './types'; import { Album, Purchase } from './types';
@ -7,9 +7,9 @@ import { Album, Purchase } from './types';
const dbPath = path.join(process.cwd(), 'data', 'parsa.db'); const dbPath = path.join(process.cwd(), 'data', 'parsa.db');
// Initialize database // Initialize database
let db: Database.Database; let db: Database;
export function getDatabase(): Database.Database { export function getDatabase(): Database {
if (!db) { if (!db) {
// Create data directory if it doesn't exist // Create data directory if it doesn't exist
const fs = require('fs'); const fs = require('fs');
@ -18,8 +18,8 @@ export function getDatabase(): Database.Database {
fs.mkdirSync(dataDir, { recursive: true }); fs.mkdirSync(dataDir, { recursive: true });
} }
db = new Database(dbPath); db = new Database(dbPath, { create: true });
db.pragma('journal_mode = WAL'); db.exec('PRAGMA journal_mode = WAL');
initializeDatabase(); initializeDatabase();
} }
return db; return db;
@ -36,6 +36,9 @@ function initializeDatabase() {
genre TEXT NOT NULL, genre TEXT NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
price REAL NOT NULL, price REAL NOT NULL,
tag TEXT NOT NULL DEFAULT 'Album',
format TEXT NOT NULL DEFAULT 'mp3',
bitrate TEXT NOT NULL DEFAULT '320kbps',
songs TEXT NOT NULL, songs TEXT NOT NULL,
createdAt INTEGER DEFAULT (strftime('%s', 'now')), createdAt INTEGER DEFAULT (strftime('%s', 'now')),
updatedAt INTEGER DEFAULT (strftime('%s', 'now')) updatedAt INTEGER DEFAULT (strftime('%s', 'now'))
@ -73,8 +76,8 @@ function initializeDatabase() {
function seedInitialData() { function seedInitialData() {
const insert = db.prepare(` const insert = db.prepare(`
INSERT INTO albums (id, title, coverImage, year, genre, description, price, songs) INSERT INTO albums (id, title, coverImage, year, genre, description, price, tag, format, bitrate, songs)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`); `);
const insertMany = db.transaction((albums: Album[]) => { const insertMany = db.transaction((albums: Album[]) => {
@ -87,6 +90,9 @@ function seedInitialData() {
album.genre, album.genre,
album.description, album.description,
album.price, album.price,
album.tag,
album.format,
album.bitrate,
JSON.stringify(album.songs) JSON.stringify(album.songs)
); );
} }
@ -119,8 +125,8 @@ export const albumDb = {
create(album: Album): void { create(album: Album): void {
const db = getDatabase(); const db = getDatabase();
db.prepare(` db.prepare(`
INSERT INTO albums (id, title, coverImage, year, genre, description, price, songs) INSERT INTO albums (id, title, coverImage, year, genre, description, price, tag, format, bitrate, songs)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run( `).run(
album.id, album.id,
album.title, album.title,
@ -129,6 +135,9 @@ export const albumDb = {
album.genre, album.genre,
album.description, album.description,
album.price, album.price,
album.tag,
album.format,
album.bitrate,
JSON.stringify(album.songs) JSON.stringify(album.songs)
); );
}, },
@ -137,7 +146,7 @@ export const albumDb = {
const db = getDatabase(); const db = getDatabase();
db.prepare(` db.prepare(`
UPDATE albums UPDATE albums
SET title = ?, coverImage = ?, year = ?, genre = ?, description = ?, price = ?, songs = ?, updatedAt = strftime('%s', 'now') SET title = ?, coverImage = ?, year = ?, genre = ?, description = ?, price = ?, tag = ?, format = ?, bitrate = ?, songs = ?, updatedAt = strftime('%s', 'now')
WHERE id = ? WHERE id = ?
`).run( `).run(
album.title, album.title,
@ -146,6 +155,9 @@ export const albumDb = {
album.genre, album.genre,
album.description, album.description,
album.price, album.price,
album.tag,
album.format,
album.bitrate,
JSON.stringify(album.songs), JSON.stringify(album.songs),
id id
); );

View File

@ -1,3 +1,6 @@
export type AlbumTag = 'Album' | 'EP' | 'Demo' | 'Deluxe' | 'Single';
export type AudioFormat = 'mp3' | 'm4a' | 'flac' | 'wav';
export interface Song { export interface Song {
id: string; id: string;
title: string; title: string;
@ -15,6 +18,9 @@ export interface Album {
price: number; price: number;
description: string; description: string;
songs: Song[]; songs: Song[];
tag: AlbumTag; // EP, Demo, Deluxe, etc.
format: AudioFormat; // mp3, m4a, flac, wav
bitrate: string; // e.g., "320kbps", "lossless"
} }
export interface Purchase { export interface Purchase {

8
lib/utils.ts Normal file
View File

@ -0,0 +1,8 @@
/**
* Format a price in Toman with comma separators
* @param price - Price in Toman
* @returns Formatted price string with تومان suffix
*/
export function formatPrice(price: number): string {
return `${price.toLocaleString('fa-IR')} تومان`;
}

8387
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,12 +6,12 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint",
"seed": "bun run scripts/seed.ts"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.937.0", "@aws-sdk/client-s3": "^3.937.0",
"@aws-sdk/s3-request-presigner": "^3.937.0", "@aws-sdk/s3-request-presigner": "^3.937.0",
"better-sqlite3": "^12.4.5",
"framer-motion": "^11.11.17", "framer-motion": "^11.11.17",
"next": "^15.0.0", "next": "^15.0.0",
"react": "^19.0.0", "react": "^19.0.0",
@ -19,7 +19,7 @@
"react-icons": "^5.3.0" "react-icons": "^5.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.13", "@types/bun": "latest",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",

265
scripts/seed.js Normal file
View File

@ -0,0 +1,265 @@
const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
// Demo albums data
const albums = [
{
id: "echoes-of-time",
title: "Echoes of Time",
coverImage: "/albums/echoes-of-time.jpg",
year: 2024,
genre: "Progressive Rock",
price: 350000,
tag: "Album",
format: "flac",
bitrate: "lossless",
description: "An epic journey through time and space, featuring complex polyrhythms, atmospheric keyboards, and powerful guitar solos. This concept album tells the story of humanity's relationship with time.",
songs: [
{
id: "echoes-1",
title: "Temporal Flux",
duration: "8:45",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/echoes-1-full.mp3"
},
{
id: "echoes-2",
title: "Clockwork Dreams",
duration: "6:23",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/echoes-2-full.mp3"
},
{
id: "echoes-3",
title: "The Eternal Now",
duration: "12:17",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/echoes-3-full.mp3"
},
{
id: "echoes-4",
title: "Yesterday's Tomorrow",
duration: "7:56",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/echoes-4-full.mp3"
},
{
id: "echoes-5",
title: "Echoes Fade",
duration: "15:32",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/echoes-5-full.mp3"
}
]
},
{
id: "crimson-horizons",
title: "Crimson Horizons",
coverImage: "/albums/crimson-horizons.jpg",
year: 2023,
genre: "Progressive Rock",
price: 10.99,
tag: "Deluxe",
format: "mp3",
bitrate: "320kbps",
description: "A darker, heavier exploration of prog rock with crushing riffs and intricate instrumental passages. Features extended improvisational sections and powerful vocals.",
songs: [
{
id: "crimson-1",
title: "Red Dawn",
duration: "9:12",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/crimson-1-full.mp3"
},
{
id: "crimson-2",
title: "Horizon's Edge",
duration: "7:45",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/crimson-2-full.mp3"
},
{
id: "crimson-3",
title: "Scarlet Skies",
duration: "11:03",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/crimson-3-full.mp3"
},
{
id: "crimson-4",
title: "Blood Moon Rising",
duration: "8:34",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/crimson-4-full.mp3"
},
{
id: "crimson-5",
title: "Into the Void",
duration: "13:21",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/crimson-5-full.mp3"
}
]
},
{
id: "cosmic-resonance",
title: "Cosmic Resonance",
coverImage: "/albums/cosmic-resonance.jpg",
year: 2022,
genre: "Space Rock",
price: 11.99,
tag: "EP",
format: "wav",
bitrate: "lossless",
description: "A space-themed odyssey combining ambient soundscapes with progressive rock energy. Synthesizers and guitars intertwine to create an otherworldly sonic experience.",
songs: [
{
id: "cosmic-1",
title: "Stellar Winds",
duration: "10:24",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/cosmic-1-full.mp3"
},
{
id: "cosmic-2",
title: "Nebula Dreams",
duration: "8:17",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/cosmic-2-full.mp3"
},
{
id: "cosmic-3",
title: "Gravity Wells",
duration: "9:45",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/cosmic-3-full.mp3"
},
{
id: "cosmic-4",
title: "Cosmic Dance",
duration: "11:56",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/cosmic-4-full.mp3"
}
]
}
];
function seedDatabase() {
try {
// Database path
const dbPath = path.join(process.cwd(), 'data', 'parsa.db');
// Create data directory if it doesn't exist
const dataDir = path.join(process.cwd(), 'data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
console.log('✓ Created data directory');
}
// Initialize database
const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
console.log('✓ Connected to database');
// Create albums table
db.exec(`
CREATE TABLE IF NOT EXISTS albums (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
coverImage TEXT NOT NULL,
year INTEGER NOT NULL,
genre TEXT NOT NULL,
description TEXT NOT NULL,
price REAL NOT NULL,
tag TEXT NOT NULL DEFAULT 'Album',
format TEXT NOT NULL DEFAULT 'mp3',
bitrate TEXT NOT NULL DEFAULT '320kbps',
songs TEXT NOT NULL,
createdAt INTEGER DEFAULT (strftime('%s', 'now')),
updatedAt INTEGER DEFAULT (strftime('%s', 'now'))
)
`);
console.log('✓ Created albums table');
// Create purchases table
db.exec(`
CREATE TABLE IF NOT EXISTS purchases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
albumId TEXT NOT NULL,
transactionId TEXT NOT NULL UNIQUE,
customerName TEXT,
email TEXT,
phoneNumber TEXT,
txReceipt TEXT,
purchaseDate INTEGER NOT NULL,
createdAt INTEGER DEFAULT (strftime('%s', 'now')),
FOREIGN KEY (albumId) REFERENCES albums(id) ON DELETE CASCADE
)
`);
console.log('✓ Created purchases table');
// Create indexes
db.exec(`
CREATE INDEX IF NOT EXISTS idx_purchases_albumId ON purchases(albumId);
CREATE INDEX IF NOT EXISTS idx_purchases_transactionId ON purchases(transactionId);
`);
console.log('✓ Created indexes');
// Clear existing albums
db.exec('DELETE FROM albums');
console.log('✓ Cleared existing albums');
// Insert demo albums
const insert = db.prepare(`
INSERT INTO albums (id, title, coverImage, year, genre, description, price, tag, format, bitrate, songs)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const insertMany = db.transaction((albums) => {
for (const album of albums) {
insert.run(
album.id,
album.title,
album.coverImage,
album.year,
album.genre,
album.description,
album.price,
album.tag,
album.format,
album.bitrate,
JSON.stringify(album.songs)
);
}
});
insertMany(albums);
console.log(`✓ Inserted ${albums.length} demo albums`);
// Verify
const count = db.prepare('SELECT COUNT(*) as count FROM albums').get();
console.log(`✓ Database now contains ${count.count} albums`);
db.close();
console.log('\n✅ Database seeded successfully!');
console.log('\nYou can now run: pnpm dev');
} catch (error) {
console.error('\n❌ Error seeding database:', error.message);
console.error('\nTrying to rebuild better-sqlite3...');
const { execSync } = require('child_process');
try {
execSync('npm rebuild better-sqlite3', { stdio: 'inherit' });
console.log('\n✓ Rebuilt better-sqlite3, please run the seed script again: pnpm seed');
} catch (rebuildError) {
console.error('❌ Failed to rebuild better-sqlite3');
console.error('Please try manually: npm rebuild better-sqlite3');
}
process.exit(1);
}
}
seedDatabase();

252
scripts/seed.ts Normal file
View File

@ -0,0 +1,252 @@
import { Database } from 'bun:sqlite';
import path from 'path';
import { existsSync, mkdirSync } from 'fs';
// Demo albums data
const albums = [
{
id: "echoes-of-time",
title: "Echoes of Time",
coverImage: "/albums/echoes-of-time.jpg",
year: 2024,
genre: "Progressive Rock",
price: 350000, // Toman
tag: "Album",
format: "flac",
bitrate: "lossless",
description: "An epic journey through time and space, featuring complex polyrhythms, atmospheric keyboards, and powerful guitar solos. This concept album tells the story of humanity's relationship with time.",
songs: [
{
id: "echoes-1",
title: "Temporal Flux",
duration: "8:45",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/echoes-1-full.mp3"
},
{
id: "echoes-2",
title: "Clockwork Dreams",
duration: "6:23",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/echoes-2-full.mp3"
},
{
id: "echoes-3",
title: "The Eternal Now",
duration: "12:17",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/echoes-3-full.mp3"
},
{
id: "echoes-4",
title: "Yesterday's Tomorrow",
duration: "7:56",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/echoes-4-full.mp3"
},
{
id: "echoes-5",
title: "Echoes Fade",
duration: "15:32",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/echoes-5-full.mp3"
}
]
},
{
id: "crimson-horizons",
title: "Crimson Horizons",
coverImage: "/albums/crimson-horizons.jpg",
year: 2023,
genre: "Progressive Rock",
price: 450000, // Toman
tag: "Deluxe",
format: "mp3",
bitrate: "320kbps",
description: "A darker, heavier exploration of prog rock with crushing riffs and intricate instrumental passages. Features extended improvisational sections and powerful vocals.",
songs: [
{
id: "crimson-1",
title: "Red Dawn",
duration: "9:12",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/crimson-1-full.mp3"
},
{
id: "crimson-2",
title: "Horizon's Edge",
duration: "7:45",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/crimson-2-full.mp3"
},
{
id: "crimson-3",
title: "Scarlet Skies",
duration: "11:03",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/crimson-3-full.mp3"
},
{
id: "crimson-4",
title: "Blood Moon Rising",
duration: "8:34",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/crimson-4-full.mp3"
},
{
id: "crimson-5",
title: "Into the Void",
duration: "13:21",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/crimson-5-full.mp3"
}
]
},
{
id: "cosmic-resonance",
title: "Cosmic Resonance",
coverImage: "/albums/cosmic-resonance.jpg",
year: 2022,
genre: "Space Rock",
price: 150000, // Toman
tag: "EP",
format: "wav",
bitrate: "lossless",
description: "A space-themed odyssey combining ambient soundscapes with progressive rock energy. Synthesizers and guitars intertwine to create an otherworldly sonic experience.",
songs: [
{
id: "cosmic-1",
title: "Stellar Winds",
duration: "10:24",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/cosmic-1-full.mp3"
},
{
id: "cosmic-2",
title: "Nebula Dreams",
duration: "8:17",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/cosmic-2-full.mp3"
},
{
id: "cosmic-3",
title: "Gravity Wells",
duration: "9:45",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/cosmic-3-full.mp3"
},
{
id: "cosmic-4",
title: "Cosmic Dance",
duration: "11:56",
previewUrl: "/audio/preview-1.mp3",
fullUrl: "/audio/cosmic-4-full.mp3"
}
]
}
];
function seedDatabase() {
try {
// Database path
const dbPath = path.join(process.cwd(), 'data', 'parsa.db');
// Create data directory if it doesn't exist
const dataDir = path.join(process.cwd(), 'data');
if (!existsSync(dataDir)) {
mkdirSync(dataDir, { recursive: true });
console.log('✓ Created data directory');
}
// Initialize database
const db = new Database(dbPath, { create: true });
db.exec('PRAGMA journal_mode = WAL');
console.log('✓ Connected to database');
// Create albums table
db.exec(`
CREATE TABLE IF NOT EXISTS albums (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
coverImage TEXT NOT NULL,
year INTEGER NOT NULL,
genre TEXT NOT NULL,
description TEXT NOT NULL,
price REAL NOT NULL,
tag TEXT NOT NULL DEFAULT 'Album',
format TEXT NOT NULL DEFAULT 'mp3',
bitrate TEXT NOT NULL DEFAULT '320kbps',
songs TEXT NOT NULL,
createdAt INTEGER DEFAULT (strftime('%s', 'now')),
updatedAt INTEGER DEFAULT (strftime('%s', 'now'))
)
`);
console.log('✓ Created albums table');
// Create purchases table
db.exec(`
CREATE TABLE IF NOT EXISTS purchases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
albumId TEXT NOT NULL,
transactionId TEXT NOT NULL UNIQUE,
customerName TEXT,
email TEXT,
phoneNumber TEXT,
txReceipt TEXT,
purchaseDate INTEGER NOT NULL,
createdAt INTEGER DEFAULT (strftime('%s', 'now')),
FOREIGN KEY (albumId) REFERENCES albums(id) ON DELETE CASCADE
)
`);
console.log('✓ Created purchases table');
// Create indexes
db.exec(`
CREATE INDEX IF NOT EXISTS idx_purchases_albumId ON purchases(albumId);
CREATE INDEX IF NOT EXISTS idx_purchases_transactionId ON purchases(transactionId);
`);
console.log('✓ Created indexes');
// Clear existing albums
db.exec('DELETE FROM albums');
console.log('✓ Cleared existing albums');
// Insert demo albums
const insert = db.prepare(`
INSERT INTO albums (id, title, coverImage, year, genre, description, price, tag, format, bitrate, songs)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const album of albums) {
insert.run(
album.id,
album.title,
album.coverImage,
album.year,
album.genre,
album.description,
album.price,
album.tag,
album.format,
album.bitrate,
JSON.stringify(album.songs)
);
}
console.log(`✓ Inserted ${albums.length} demo albums`);
// Verify
const count = db.prepare('SELECT COUNT(*) as count FROM albums').get() as { count: number };
console.log(`✓ Database now contains ${count.count} albums`);
db.close();
console.log('\n✅ Database seeded successfully!');
console.log('\nYou can now run: bun dev');
} catch (error) {
console.error('\n❌ Error seeding database:', error);
process.exit(1);
}
}
seedDatabase();

View File

@ -9,25 +9,21 @@ const config: Config = {
theme: { theme: {
extend: { extend: {
colors: { colors: {
primary: { paper: {
50: '#e6f1ff', light: '#D9DACA', // Light cream/off-white
100: '#b3d9ff', sand: '#B1B09C', // Muted beige/tan
200: '#80c1ff', gray: '#848170', // Grayish olive
300: '#4da8ff', brown: '#5A554C', // Medium brown/gray
400: '#1a90ff', dark: '#2F2926', // Dark brown/charcoal
500: '#0077e6',
600: '#005db3',
700: '#004380',
800: '#00294d',
900: '#000f1a',
},
accent: {
orange: '#ff6b35',
cyan: '#00d9ff',
}, },
}, },
backgroundImage: { backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'paper-texture': "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' /%3E%3C/filter%3E%3Crect width='100' height='100' filter='url(%23noise)' opacity='0.05'/%3E%3C/svg%3E\")",
},
boxShadow: {
'paper': '2px 2px 0px rgba(47, 41, 38, 0.3)',
'paper-lg': '4px 4px 0px rgba(47, 41, 38, 0.3)',
'paper-inset': 'inset 2px 2px 4px rgba(47, 41, 38, 0.2)',
}, },
}, },
}, },