-
Transaction ID
-
{purchase.transactionId}
-
+
+
Transaction ID / شناسه تراکنش
+
{purchase.transactionId}
+
{new Date(purchase.purchaseDate).toLocaleString()}
@@ -120,25 +122,26 @@ You will receive access to this album after confirmation.
- Download Receipt
+ Download Receipt / دریافت رسید
- OK, Got It
+ OK, Got It / متوجه شدم
{/* Info */}
-
- Your purchase will be reviewed by an admin. You will receive access after approval.
+
+ Your purchase will be reviewed by an admin. You will receive access after approval.
+ سفارش شما توسط مدیر بررسی میشود. پس از تایید به آلبوم دسترسی خواهید داشت.
diff --git a/lib/data.ts b/lib/data.ts
index 38683be..a97095c 100644
--- a/lib/data.ts
+++ b/lib/data.ts
@@ -1,9 +1,10 @@
import { Album } from './types';
export const artistBio = {
- name: "Parsa",
+ name: "Parsa Sadatie",
+ nickname: "@parsadat",
title: "Progressive Rock Composer & Producer",
- bio: `A visionary composer and producer specializing in progressive rock and rock music. With a career spanning over a decade, Parsa has crafted intricate soundscapes that blend complex time signatures, soaring melodies, and powerful instrumentation. His work pushes the boundaries of rock music, drawing inspiration from classic prog rock pioneers while incorporating modern production techniques.
+ bio: `A visionary composer and producer specializing in progressive rock and rock music. With a career spanning over a decade, Parsa Sadatie has crafted intricate soundscapes that blend complex time signatures, soaring melodies, and powerful instrumentation. His work pushes the boundaries of rock music, drawing inspiration from classic prog rock pioneers while incorporating modern production techniques.
Each album is a journey through sonic landscapes, featuring elaborate compositions that challenge and reward the listener. From epic 20-minute suites to more concise rock anthems, every piece is meticulously crafted with attention to detail and emotional depth.`,
image: "/artist-photo.jpg", // Placeholder
diff --git a/lib/db.ts b/lib/db.ts
index a71580a..b90311f 100644
--- a/lib/db.ts
+++ b/lib/db.ts
@@ -1,25 +1,40 @@
-import { Database } from 'bun:sqlite';
-import path from 'path';
-import { albums as initialAlbums } from './data';
-import { Album, Purchase } from './types';
+import { Database } from "bun:sqlite";
+import path from "path";
+import { albums as initialAlbums } from "./data";
+import { Album, Purchase } from "./types";
// Database path
-const dbPath = path.join(process.cwd(), 'data', 'parsa.db');
+const dbPath = path.join(process.cwd(), "data", "parsa.db");
// Initialize database
-let db: Database;
+let db: any;
-export function getDatabase(): Database {
+function createDatabase() {
+ // Use Bun's native SQLite if available, otherwise use better-sqlite3
+ const isBun = typeof Bun !== "undefined";
+ const { Database } = require("bun:sqlite");
+ return new Database(dbPath, { create: true });
+
+ if (isBun) {
+ const { Database } = require("bun:sqlite");
+ return new Database(dbPath, { create: true });
+ } else {
+ const Database = require("better-sqlite3");
+ return new Database(dbPath);
+ }
+}
+
+export function getDatabase(): any {
if (!db) {
// Create data directory if it doesn't exist
- const fs = require('fs');
- const dataDir = path.join(process.cwd(), 'data');
+ const fs = require("fs");
+ const dataDir = path.join(process.cwd(), "data");
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
- db = new Database(dbPath, { create: true });
- db.exec('PRAGMA journal_mode = WAL');
+ db = createDatabase();
+ db.exec("PRAGMA journal_mode = WAL");
initializeDatabase();
}
return db;
@@ -56,19 +71,58 @@ function initializeDatabase() {
phoneNumber TEXT,
txReceipt TEXT,
purchaseDate INTEGER NOT NULL,
+ approvalStatus TEXT DEFAULT 'pending',
+ paymentMethod TEXT DEFAULT 'card-to-card',
createdAt INTEGER DEFAULT (strftime('%s', 'now')),
FOREIGN KEY (albumId) REFERENCES albums(id) ON DELETE CASCADE
)
`);
+ // Create payment authorities table for ZarinPal tracking
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS payment_authorities (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ authority TEXT NOT NULL UNIQUE,
+ albumId TEXT NOT NULL,
+ amount INTEGER NOT NULL,
+ customerName TEXT,
+ email TEXT,
+ phoneNumber TEXT,
+ status TEXT DEFAULT 'pending',
+ refId TEXT,
+ cardPan TEXT,
+ fee INTEGER,
+ createdAt INTEGER DEFAULT (strftime('%s', 'now')),
+ verifiedAt INTEGER,
+ FOREIGN KEY (albumId) REFERENCES albums(id) ON DELETE CASCADE
+ )
+ `);
+
+ // Add columns if they don't exist (migration)
+ try {
+ db.exec(`ALTER TABLE purchases ADD COLUMN approvalStatus TEXT DEFAULT 'pending'`);
+ } catch (e) {
+ // Column already exists
+ }
+ try {
+ db.exec(`ALTER TABLE purchases ADD COLUMN paymentMethod TEXT DEFAULT 'card-to-card'`);
+ } catch (e) {
+ // Column already exists
+ }
+
// 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);
+ CREATE INDEX IF NOT EXISTS idx_purchases_approvalStatus ON purchases(approvalStatus);
+ CREATE INDEX IF NOT EXISTS idx_payment_authorities_authority ON payment_authorities(authority);
+ CREATE INDEX IF NOT EXISTS idx_payment_authorities_status ON payment_authorities(status);
`);
// Seed initial data if albums table is empty
- const count = db.prepare('SELECT COUNT(*) as count FROM albums').get() as { count: number };
+ const count = db.prepare("SELECT COUNT(*) as count FROM albums").get() as {
+ count: number;
+ };
if (count.count === 0) {
seedInitialData();
}
@@ -93,7 +147,7 @@ function seedInitialData() {
album.tag,
album.format,
album.bitrate,
- JSON.stringify(album.songs)
+ JSON.stringify(album.songs),
);
}
});
@@ -105,7 +159,7 @@ function seedInitialData() {
export const albumDb = {
getAll(): Album[] {
const db = getDatabase();
- const rows = db.prepare('SELECT * FROM albums ORDER BY year DESC').all();
+ const rows = db.prepare("SELECT * FROM albums ORDER BY year DESC").all();
return rows.map((row: any) => ({
...row,
songs: JSON.parse(row.songs),
@@ -114,7 +168,7 @@ export const albumDb = {
getById(id: string): Album | null {
const db = getDatabase();
- const row = db.prepare('SELECT * FROM albums WHERE id = ?').get(id) as any;
+ const row = db.prepare("SELECT * FROM albums WHERE id = ?").get(id) as any;
if (!row) return null;
return {
...row,
@@ -124,10 +178,12 @@ export const albumDb = {
create(album: Album): void {
const db = getDatabase();
- db.prepare(`
+ db.prepare(
+ `
INSERT INTO albums (id, title, coverImage, year, genre, description, price, tag, format, bitrate, songs)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- `).run(
+ `,
+ ).run(
album.id,
album.title,
album.coverImage,
@@ -138,17 +194,19 @@ export const albumDb = {
album.tag,
album.format,
album.bitrate,
- JSON.stringify(album.songs)
+ JSON.stringify(album.songs),
);
},
update(id: string, album: Album): void {
const db = getDatabase();
- db.prepare(`
+ db.prepare(
+ `
UPDATE albums
SET title = ?, coverImage = ?, year = ?, genre = ?, description = ?, price = ?, tag = ?, format = ?, bitrate = ?, songs = ?, updatedAt = strftime('%s', 'now')
WHERE id = ?
- `).run(
+ `,
+ ).run(
album.title,
album.coverImage,
album.year,
@@ -159,13 +217,13 @@ export const albumDb = {
album.format,
album.bitrate,
JSON.stringify(album.songs),
- id
+ id,
);
},
delete(id: string): void {
const db = getDatabase();
- db.prepare('DELETE FROM albums WHERE id = ?').run(id);
+ db.prepare("DELETE FROM albums WHERE id = ?").run(id);
},
};
@@ -173,7 +231,9 @@ export const albumDb = {
export const purchaseDb = {
getAll(): Purchase[] {
const db = getDatabase();
- const rows = db.prepare('SELECT * FROM purchases ORDER BY purchaseDate DESC').all();
+ const rows = db
+ .prepare("SELECT * FROM purchases ORDER BY purchaseDate DESC")
+ .all();
return rows.map((row: any) => ({
id: row.id,
albumId: row.albumId,
@@ -183,12 +243,18 @@ export const purchaseDb = {
phoneNumber: row.phoneNumber,
txReceipt: row.txReceipt,
purchaseDate: new Date(row.purchaseDate),
+ approvalStatus: row.approvalStatus,
+ paymentMethod: row.paymentMethod,
}));
},
getByAlbumId(albumId: string): Purchase[] {
const db = getDatabase();
- const rows = db.prepare('SELECT * FROM purchases WHERE albumId = ? ORDER BY purchaseDate DESC').all(albumId);
+ const rows = db
+ .prepare(
+ "SELECT * FROM purchases WHERE albumId = ? ORDER BY purchaseDate DESC",
+ )
+ .all(albumId);
return rows.map((row: any) => ({
id: row.id,
albumId: row.albumId,
@@ -198,12 +264,16 @@ export const purchaseDb = {
phoneNumber: row.phoneNumber,
txReceipt: row.txReceipt,
purchaseDate: new Date(row.purchaseDate),
+ approvalStatus: row.approvalStatus,
+ paymentMethod: row.paymentMethod,
}));
},
getByTransactionId(transactionId: string): Purchase | null {
const db = getDatabase();
- const row = db.prepare('SELECT * FROM purchases WHERE transactionId = ?').get(transactionId) as any;
+ const row = db
+ .prepare("SELECT * FROM purchases WHERE transactionId = ?")
+ .get(transactionId) as any;
if (!row) return null;
return {
id: row.id,
@@ -214,23 +284,33 @@ export const purchaseDb = {
phoneNumber: row.phoneNumber,
txReceipt: row.txReceipt,
purchaseDate: new Date(row.purchaseDate),
+ approvalStatus: row.approvalStatus,
+ paymentMethod: row.paymentMethod,
};
},
- create(purchase: Omit
): Purchase {
+ create(purchase: Omit): Purchase {
const db = getDatabase();
- const result = db.prepare(`
- INSERT INTO purchases (albumId, transactionId, customerName, email, phoneNumber, txReceipt, purchaseDate)
- VALUES (?, ?, ?, ?, ?, ?, ?)
- `).run(
- purchase.albumId,
- purchase.transactionId,
- purchase.customerName || null,
- purchase.email || null,
- purchase.phoneNumber || null,
- purchase.txReceipt || null,
- purchase.purchaseDate instanceof Date ? purchase.purchaseDate.getTime() : purchase.purchaseDate
- );
+ const result = db
+ .prepare(
+ `
+ INSERT INTO purchases (albumId, transactionId, customerName, email, phoneNumber, txReceipt, purchaseDate, approvalStatus, paymentMethod)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `,
+ )
+ .run(
+ purchase.albumId,
+ purchase.transactionId,
+ purchase.customerName || null,
+ purchase.email || null,
+ purchase.phoneNumber || null,
+ purchase.txReceipt || null,
+ purchase.purchaseDate instanceof Date
+ ? purchase.purchaseDate.getTime()
+ : purchase.purchaseDate,
+ purchase.approvalStatus || 'pending',
+ purchase.paymentMethod || 'card-to-card',
+ );
return {
id: result.lastInsertRowid as number,
@@ -240,6 +320,6 @@ export const purchaseDb = {
delete(id: number): void {
const db = getDatabase();
- db.prepare('DELETE FROM purchases WHERE id = ?').run(id);
+ db.prepare("DELETE FROM purchases WHERE id = ?").run(id);
},
};
diff --git a/lib/types.ts b/lib/types.ts
index b4d070a..6a32efa 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -23,6 +23,9 @@ export interface Album {
bitrate: string; // e.g., "320kbps", "lossless"
}
+export type PurchaseStatus = 'pending' | 'approved' | 'rejected';
+export type PaymentMethod = 'ipg' | 'card-to-card';
+
export interface Purchase {
id?: number;
albumId: string;
@@ -32,4 +35,6 @@ export interface Purchase {
phoneNumber?: string;
txReceipt?: string;
purchaseDate: Date | number;
+ approvalStatus?: PurchaseStatus; // pending, approved, rejected
+ paymentMethod?: PaymentMethod; // ipg or card-to-card
}
diff --git a/package.json b/package.json
index 6ddb5d4..da87f04 100644
--- a/package.json
+++ b/package.json
@@ -12,14 +12,17 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.937.0",
"@aws-sdk/s3-request-presigner": "^3.937.0",
+ "better-sqlite3": "^11.7.0",
"framer-motion": "^11.11.17",
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
- "react-icons": "^5.3.0"
+ "react-icons": "^5.3.0",
+ "zarinpal-node-sdk": "^2.2.0"
},
"devDependencies": {
- "@types/bun": "latest",
+ "@types/better-sqlite3": "^7.6.12",
+ "@types/bun": "^1.3.5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
diff --git a/tsconfig.json b/tsconfig.json
index d8b9323..794fef9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -22,6 +22,9 @@
"@/*": ["./*"]
}
},
+ "types": [
+ "bun-types" // add Bun global
+ ],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}