main: second iter

Signed-off-by: nfel <nfilsaraee@gmail.com>
This commit is contained in:
2025-12-27 22:41:36 +03:30
parent 8a7842e263
commit 9a7e627329
33 changed files with 4865 additions and 81 deletions
+54
View File
@@ -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;
}
+116
View File
@@ -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;
}
+226
View File
@@ -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();
+80
View File
@@ -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
View File
@@ -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;
}