@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
|
||||
interface AdminContextType {
|
||||
isAuthenticated: boolean;
|
||||
login: (username: string, password: string) => boolean;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
const AdminContext = createContext<AdminContextType | undefined>(undefined);
|
||||
|
||||
// Demo credentials - In production, use proper authentication
|
||||
const ADMIN_USERNAME = 'admin';
|
||||
const ADMIN_PASSWORD = 'admin123';
|
||||
|
||||
export function AdminProvider({ children }: { children: ReactNode }) {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const authStatus = localStorage.getItem('adminAuth');
|
||||
if (authStatus === 'true') {
|
||||
setIsAuthenticated(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const login = (username: string, password: string): boolean => {
|
||||
if (username === ADMIN_USERNAME && password === ADMIN_PASSWORD) {
|
||||
setIsAuthenticated(true);
|
||||
localStorage.setItem('adminAuth', 'true');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setIsAuthenticated(false);
|
||||
localStorage.removeItem('adminAuth');
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminContext.Provider value={{ isAuthenticated, login, logout }}>
|
||||
{children}
|
||||
</AdminContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAdmin() {
|
||||
const context = useContext(AdminContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAdmin must be used within an AdminProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { Album } from './types';
|
||||
|
||||
interface AlbumsContextType {
|
||||
albums: Album[];
|
||||
loading: boolean;
|
||||
addAlbum: (album: Album) => Promise<void>;
|
||||
updateAlbum: (albumId: string, updatedAlbum: Album) => Promise<void>;
|
||||
deleteAlbum: (albumId: string) => Promise<void>;
|
||||
getAlbumById: (albumId: string) => Album | undefined;
|
||||
refreshAlbums: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AlbumsContext = createContext<AlbumsContextType | undefined>(undefined);
|
||||
|
||||
export function AlbumsProvider({ children }: { children: ReactNode }) {
|
||||
const [albums, setAlbums] = useState<Album[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Fetch albums from API
|
||||
const fetchAlbums = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/albums');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch albums');
|
||||
}
|
||||
const data = await response.json();
|
||||
setAlbums(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching albums:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load albums on mount
|
||||
useEffect(() => {
|
||||
fetchAlbums();
|
||||
}, []);
|
||||
|
||||
const addAlbum = async (album: Album) => {
|
||||
try {
|
||||
const response = await fetch('/api/albums', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(album),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add album');
|
||||
}
|
||||
|
||||
await fetchAlbums(); // Refresh the list
|
||||
} catch (error) {
|
||||
console.error('Error adding album:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const updateAlbum = async (albumId: string, updatedAlbum: Album) => {
|
||||
try {
|
||||
const response = await fetch('/api/albums', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updatedAlbum),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update album');
|
||||
}
|
||||
|
||||
await fetchAlbums(); // Refresh the list
|
||||
} catch (error) {
|
||||
console.error('Error updating album:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteAlbum = async (albumId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/albums?id=${albumId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete album');
|
||||
}
|
||||
|
||||
await fetchAlbums(); // Refresh the list
|
||||
} catch (error) {
|
||||
console.error('Error deleting album:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getAlbumById = (albumId: string) => {
|
||||
return albums.find((album) => album.id === albumId);
|
||||
};
|
||||
|
||||
return (
|
||||
<AlbumsContext.Provider value={{ albums, loading, addAlbum, updateAlbum, deleteAlbum, getAlbumById, refreshAlbums: fetchAlbums }}>
|
||||
{children}
|
||||
</AlbumsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAlbums() {
|
||||
const context = useContext(AlbumsContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAlbums must be used within an AlbumsProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import Database from 'better-sqlite3';
|
||||
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');
|
||||
|
||||
// Initialize database
|
||||
let db: Database.Database;
|
||||
|
||||
export function getDatabase(): Database.Database {
|
||||
if (!db) {
|
||||
// Create data directory if it doesn't exist
|
||||
const fs = require('fs');
|
||||
const dataDir = path.join(process.cwd(), 'data');
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
initializeDatabase();
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
function initializeDatabase() {
|
||||
// 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,
|
||||
songs TEXT NOT NULL,
|
||||
createdAt INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
updatedAt INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// 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
|
||||
)
|
||||
`);
|
||||
|
||||
// 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);
|
||||
`);
|
||||
|
||||
// Seed initial data if albums table is empty
|
||||
const count = db.prepare('SELECT COUNT(*) as count FROM albums').get() as { count: number };
|
||||
if (count.count === 0) {
|
||||
seedInitialData();
|
||||
}
|
||||
}
|
||||
|
||||
function seedInitialData() {
|
||||
const insert = db.prepare(`
|
||||
INSERT INTO albums (id, title, coverImage, year, genre, description, price, songs)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const insertMany = db.transaction((albums: Album[]) => {
|
||||
for (const album of albums) {
|
||||
insert.run(
|
||||
album.id,
|
||||
album.title,
|
||||
album.coverImage,
|
||||
album.year,
|
||||
album.genre,
|
||||
album.description,
|
||||
album.price,
|
||||
JSON.stringify(album.songs)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
insertMany(initialAlbums);
|
||||
}
|
||||
|
||||
// Album operations
|
||||
export const albumDb = {
|
||||
getAll(): Album[] {
|
||||
const rows = db.prepare('SELECT * FROM albums ORDER BY year DESC').all();
|
||||
return rows.map((row: any) => ({
|
||||
...row,
|
||||
songs: JSON.parse(row.songs),
|
||||
}));
|
||||
},
|
||||
|
||||
getById(id: string): Album | null {
|
||||
const row = db.prepare('SELECT * FROM albums WHERE id = ?').get(id) as any;
|
||||
if (!row) return null;
|
||||
return {
|
||||
...row,
|
||||
songs: JSON.parse(row.songs),
|
||||
};
|
||||
},
|
||||
|
||||
create(album: Album): void {
|
||||
db.prepare(`
|
||||
INSERT INTO albums (id, title, coverImage, year, genre, description, price, songs)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
album.id,
|
||||
album.title,
|
||||
album.coverImage,
|
||||
album.year,
|
||||
album.genre,
|
||||
album.description,
|
||||
album.price,
|
||||
JSON.stringify(album.songs)
|
||||
);
|
||||
},
|
||||
|
||||
update(id: string, album: Album): void {
|
||||
db.prepare(`
|
||||
UPDATE albums
|
||||
SET title = ?, coverImage = ?, year = ?, genre = ?, description = ?, price = ?, songs = ?, updatedAt = strftime('%s', 'now')
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
album.title,
|
||||
album.coverImage,
|
||||
album.year,
|
||||
album.genre,
|
||||
album.description,
|
||||
album.price,
|
||||
JSON.stringify(album.songs),
|
||||
id
|
||||
);
|
||||
},
|
||||
|
||||
delete(id: string): void {
|
||||
db.prepare('DELETE FROM albums WHERE id = ?').run(id);
|
||||
},
|
||||
};
|
||||
|
||||
// Purchase operations
|
||||
export const purchaseDb = {
|
||||
getAll(): Purchase[] {
|
||||
const rows = db.prepare('SELECT * FROM purchases ORDER BY purchaseDate DESC').all();
|
||||
return rows.map((row: any) => ({
|
||||
id: row.id,
|
||||
albumId: row.albumId,
|
||||
transactionId: row.transactionId,
|
||||
customerName: row.customerName,
|
||||
email: row.email,
|
||||
phoneNumber: row.phoneNumber,
|
||||
txReceipt: row.txReceipt,
|
||||
purchaseDate: new Date(row.purchaseDate),
|
||||
}));
|
||||
},
|
||||
|
||||
getByAlbumId(albumId: string): Purchase[] {
|
||||
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,
|
||||
transactionId: row.transactionId,
|
||||
customerName: row.customerName,
|
||||
email: row.email,
|
||||
phoneNumber: row.phoneNumber,
|
||||
txReceipt: row.txReceipt,
|
||||
purchaseDate: new Date(row.purchaseDate),
|
||||
}));
|
||||
},
|
||||
|
||||
getByTransactionId(transactionId: string): Purchase | null {
|
||||
const row = db.prepare('SELECT * FROM purchases WHERE transactionId = ?').get(transactionId) as any;
|
||||
if (!row) return null;
|
||||
return {
|
||||
id: row.id,
|
||||
albumId: row.albumId,
|
||||
transactionId: row.transactionId,
|
||||
customerName: row.customerName,
|
||||
email: row.email,
|
||||
phoneNumber: row.phoneNumber,
|
||||
txReceipt: row.txReceipt,
|
||||
purchaseDate: new Date(row.purchaseDate),
|
||||
};
|
||||
},
|
||||
|
||||
create(purchase: Omit<Purchase, 'id'>): Purchase {
|
||||
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
|
||||
);
|
||||
|
||||
return {
|
||||
id: result.lastInsertRowid as number,
|
||||
...purchase,
|
||||
};
|
||||
},
|
||||
|
||||
delete(id: number): void {
|
||||
db.prepare('DELETE FROM purchases WHERE id = ?').run(id);
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize database on import
|
||||
getDatabase();
|
||||
@@ -0,0 +1,80 @@
|
||||
import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
|
||||
// Initialize S3 client
|
||||
const s3Client = new S3Client({
|
||||
region: process.env.AWS_REGION || 'ir-thr-at1',
|
||||
endpoint: process.env.AWS_S3_ENDPOINT,
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
|
||||
},
|
||||
});
|
||||
|
||||
const BUCKET_NAME = process.env.AWS_S3_BUCKET || '';
|
||||
|
||||
export interface UploadFileParams {
|
||||
file: File;
|
||||
key: string;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to S3
|
||||
*/
|
||||
export async function uploadFileToS3(params: UploadFileParams): Promise<string> {
|
||||
const { file, key, contentType } = params;
|
||||
|
||||
// Convert File to Buffer
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: BUCKET_NAME,
|
||||
Key: key,
|
||||
Body: buffer,
|
||||
ACL: "public-read",
|
||||
ContentType: contentType || file.type,
|
||||
});
|
||||
|
||||
await s3Client.send(command);
|
||||
|
||||
// Return the public URL
|
||||
return `https://${BUCKET_NAME}.s3.ir-thr-at1.arvanstorage.ir/${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file from S3
|
||||
*/
|
||||
export async function deleteFileFromS3(key: string): Promise<void> {
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: BUCKET_NAME,
|
||||
Key: key,
|
||||
});
|
||||
|
||||
await s3Client.send(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a presigned URL for uploading
|
||||
*/
|
||||
export async function getPresignedUploadUrl(key: string, contentType: string): Promise<string> {
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: BUCKET_NAME,
|
||||
Key: key,
|
||||
ContentType: contentType,
|
||||
});
|
||||
|
||||
const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 }); // 1 hour
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique key for file uploads
|
||||
*/
|
||||
export function generateFileKey(folder: string, filename: string): string {
|
||||
const timestamp = Date.now();
|
||||
const randomString = Math.random().toString(36).substring(2, 15);
|
||||
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.-]/g, '_');
|
||||
return `${folder}/${timestamp}-${randomString}-${sanitizedFilename}`;
|
||||
}
|
||||
+6
-1
@@ -18,7 +18,12 @@ export interface Album {
|
||||
}
|
||||
|
||||
export interface Purchase {
|
||||
id?: number;
|
||||
albumId: string;
|
||||
transactionId: string;
|
||||
purchaseDate: Date;
|
||||
customerName?: string;
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
txReceipt?: string;
|
||||
purchaseDate: Date | number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user