main: vibe coded app to feel like paper
Signed-off-by: nfel <nfilsaraee@gmail.com>
This commit is contained in:
parent
acbaebb947
commit
91c149e92e
24
Dockerfile
24
Dockerfile
@ -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"]
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
app/page.tsx
50
app/page.tsx
@ -71,31 +71,27 @@ 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">
|
||||||
Progressive Rock
|
<h1 className="text-5xl md:text-7xl font-bold text-paper-light mb-6 tracking-tight">
|
||||||
<br />
|
Progressive Rock
|
||||||
Reimagined
|
<br />
|
||||||
</h1>
|
Reimagined
|
||||||
<p className="text-xl md:text-2xl text-gray-300 mb-8">
|
</h1>
|
||||||
Explore intricate compositions and sonic landscapes
|
<p className="text-xl md:text-2xl text-paper-sand mb-8">
|
||||||
</p>
|
Explore intricate compositions and sonic landscapes
|
||||||
<button
|
</p>
|
||||||
onClick={() => {
|
<button
|
||||||
const element = document.getElementById('albums');
|
onClick={() => {
|
||||||
element?.scrollIntoView({ behavior: 'smooth' });
|
const element = document.getElementById('albums');
|
||||||
}}
|
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
|
>
|
||||||
</button>
|
Explore Albums
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
||||||
© {new Date().getFullYear()} Parsa. All rights reserved.
|
© {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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
15
lib/data.ts
15
lib/data.ts
@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
32
lib/db.ts
32
lib/db.ts
@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
8
lib/utils.ts
Normal 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
8387
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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
265
scripts/seed.js
Normal 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
252
scripts/seed.ts
Normal 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();
|
||||||
@ -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)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user