first commit

This commit is contained in:
hOLOlu
2026-05-31 15:23:37 +03:00
commit c7b07602b7
16 changed files with 371587 additions and 0 deletions

800
public/app.js Normal file
View File

@@ -0,0 +1,800 @@
// Global State
const state = {
currentTab: 'dashboard-tab',
dbPage: 1,
dbLimit: 12,
dbSearch: '',
dbCategory: '',
dbCompany: '',
selectedProduct: null,
sseConnected: false
};
// DOM Elements
const elements = {
tabs: document.querySelectorAll('.nav-btn'),
tabContents: document.querySelectorAll('.tab-content'),
pageTitle: document.getElementById('page-title'),
pageSubtitle: document.getElementById('page-subtitle'),
// Stats elements
statTotal: document.getElementById('stat-total'),
statCompleted: document.getElementById('stat-completed'),
statPending: document.getElementById('stat-pending'),
statFailed: document.getElementById('stat-failed'),
progressPercent: document.getElementById('progress-percent'),
progressFillSuccess: document.getElementById('progress-fill-success'),
progressFillError: document.getElementById('progress-fill-error'),
activeThreadsBadge: document.getElementById('active-threads-badge'),
// Scraper Controls
inputConcurrency: document.getElementById('input-concurrency'),
inputDelay: document.getElementById('input-delay'),
btnToggleScrape: document.getElementById('btn-toggle-scrape'),
btnRetryFailed: document.getElementById('btn-retry-failed'),
consoleLogs: document.getElementById('console-logs'),
btnClearLogs: document.getElementById('btn-clear-logs'),
// Database tab elements
dbSearchInput: document.getElementById('db-search'),
filterCategory: document.getElementById('filter-category'),
filterCompany: document.getElementById('filter-company'),
btnClearFilters: document.getElementById('btn-clear-filters'),
productsGrid: document.getElementById('products-grid-list'),
dbEmptyState: document.getElementById('db-empty-state'),
dbLoadingState: document.getElementById('db-loading-state'),
paginationControls: document.getElementById('pagination-controls'),
btnPrevPage: document.getElementById('btn-prev-page'),
btnNextPage: document.getElementById('btn-next-page'),
pageNumbersContainer: document.getElementById('page-numbers-container'),
// Form tab elements
productForm: document.getElementById('product-form'),
formProductId: document.getElementById('form-product-id'),
formBarcode: document.getElementById('form-barcode'),
formName: document.getElementById('form-name'),
formCategory: document.getElementById('form-category'),
formSubcategory: document.getElementById('form-subcategory'),
formCompany: document.getElementById('form-company'),
formImage: document.getElementById('form-image'),
btnSaveProduct: document.getElementById('btn-save-product'),
btnResetForm: document.getElementById('btn-reset-form'),
// Modal elements
productDetailModal: document.getElementById('product-detail-modal'),
btnCloseModal: document.getElementById('btn-close-modal'),
modalProductImage: document.getElementById('modal-product-image'),
modalProductName: document.getElementById('modal-product-name'),
modalProductCategory: document.getElementById('modal-product-category'),
modalProductCompany: document.getElementById('modal-product-company'),
modalCleanName: document.getElementById('modal-clean-name'),
modalAmountRaw: document.getElementById('modal-amount-raw'),
modalAmount: document.getElementById('modal-amount'),
modalUnit: document.getElementById('modal-unit'),
modalBarcodeSvg: document.getElementById('modal-barcode-svg'),
modalQrcodeCanvas: document.getElementById('modal-qrcode-canvas'),
downloadButtons: document.querySelectorAll('.download-btn'),
// Toast notification
toast: document.getElementById('toast')
};
// API Base URL
const API_URL = ''; // Same host
// Toast Notification Helper
function showToast(message, isError = false) {
const toast = elements.toast;
const icon = toast.querySelector('.toast-icon');
const msg = toast.querySelector('.toast-message');
msg.textContent = message;
if (isError) {
toast.classList.add('error');
icon.className = 'fa-solid fa-circle-xmark toast-icon';
} else {
toast.classList.remove('error');
icon.className = 'fa-solid fa-circle-check toast-icon';
}
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 4000);
}
/* Event Handlers & Core Functions */
// Initialize client
document.addEventListener('DOMContentLoaded', () => {
initTabs();
initScraperControl();
initDatabase();
initForm();
initModal();
connectSSE();
});
// 1. Navigation Tab Switches
function initTabs() {
elements.tabs.forEach(btn => {
btn.addEventListener('click', () => {
const targetTab = btn.getAttribute('data-tab');
state.currentTab = targetTab;
// Toggle active classes
elements.tabs.forEach(t => t.classList.remove('active'));
elements.tabContents.forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.getElementById(targetTab).classList.add('active');
// Header Content Updates
if (targetTab === 'dashboard-tab') {
elements.pageTitle.textContent = 'Kontrol Paneli';
elements.pageSubtitle.textContent = 'Veri tarama sürecini ve sunucu istatistiklerini canlı takip edin';
} else if (targetTab === 'database-tab') {
elements.pageTitle.textContent = 'Ürün Veritabanı';
elements.pageSubtitle.textContent = 'Taranan barkod verilerini arayın, inceleyin, düzenleyin veya yeni kodlar üretin';
fetchProducts();
} else if (targetTab === 'add-product-tab') {
elements.pageTitle.textContent = 'Ürün Ekle';
elements.pageSubtitle.textContent = 'Veritabanına manuel olarak yeni bir barkod kaydı girin';
resetForm();
}
});
});
}
// 2. SSE Scraper Stream Connection
let sseSource = null;
let pollingInterval = null;
function connectSSE() {
if (sseSource) {
sseSource.close();
}
const indicator = document.querySelector('.status-indicator');
// Set a timeout to switch to polling if SSE doesn't connect within 2.5 seconds
const sseTimeout = setTimeout(() => {
if (!state.sseConnected) {
console.log('SSE connection timeout. Falling back to HTTP polling...');
startPolling();
}
}, 2500);
try {
if (!window.EventSource) {
throw new Error('EventSource is not supported by this browser.');
}
sseSource = new EventSource(`${API_URL}/api/scrape/stream`);
sseSource.onopen = () => {
clearTimeout(sseTimeout);
stopPolling();
state.sseConnected = true;
indicator.className = 'status-indicator connected';
indicator.querySelector('.status-text').textContent = 'Sunucu Bağlantısı Aktif (SSE)';
};
sseSource.onerror = (e) => {
clearTimeout(sseTimeout);
state.sseConnected = false;
indicator.className = 'status-indicator disconnected';
indicator.querySelector('.status-text').textContent = 'SSE Bağlantısı Kesildi - Polling Aktif';
startPolling();
};
sseSource.onmessage = (event) => {
try {
const stats = JSON.parse(event.data);
updateScraperDashboard(stats);
} catch (error) {
console.error('Failed to parse SSE data:', error);
}
};
} catch (err) {
clearTimeout(sseTimeout);
console.warn('SSE initialization failed. Using polling fallback:', err.message);
startPolling();
}
}
function startPolling() {
if (pollingInterval) return;
const indicator = document.querySelector('.status-indicator');
indicator.className = 'status-indicator connected';
indicator.querySelector('.status-text').textContent = 'Sunucu Bağlantısı Aktif (Polling)';
// Run once immediately
fetchStats();
pollingInterval = setInterval(fetchStats, 1500);
}
async function fetchStats() {
const indicator = document.querySelector('.status-indicator');
try {
const response = await fetch(`${API_URL}/api/stats`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const stats = await response.json();
updateScraperDashboard(stats);
indicator.className = 'status-indicator connected';
if (!state.sseConnected) {
indicator.querySelector('.status-text').textContent = 'Sunucu Bağlantısı Aktif (Polling)';
}
} catch (error) {
console.error('Polling error:', error);
indicator.className = 'status-indicator disconnected';
indicator.querySelector('.status-text').textContent = 'Sunucu Bağlantısı Koptu';
}
}
function stopPolling() {
if (pollingInterval) {
clearInterval(pollingInterval);
pollingInterval = null;
}
}
// Update Dashboard Stats & Progress
let lastLogTimestamp = null;
function updateScraperDashboard(stats) {
// 1. Text values
elements.statTotal.textContent = stats.totalUrls.toLocaleString();
elements.statCompleted.textContent = stats.completedCount.toLocaleString();
elements.statPending.textContent = stats.pendingCount.toLocaleString();
elements.statFailed.textContent = stats.failedCount.toLocaleString();
elements.activeThreadsBadge.textContent = `${stats.activeWorkers} Aktif İşçi`;
// 2. Concurrency inputs update if not focused
if (document.activeElement !== elements.inputConcurrency) {
elements.inputConcurrency.value = stats.concurrency;
}
if (document.activeElement !== elements.inputDelay) {
elements.inputDelay.value = stats.delayMs;
}
// 3. Play/Pause Button State
if (stats.isRunning) {
elements.btnToggleScrape.className = 'btn btn-danger btn-large';
elements.btnToggleScrape.innerHTML = '<i class="fa-solid fa-pause"></i> Tarayıcıyı Duraklat';
} else {
elements.btnToggleScrape.className = 'btn btn-primary btn-large';
elements.btnToggleScrape.innerHTML = '<i class="fa-solid fa-play"></i> Tarayıcıyı Başlat';
}
// 4. Progress Bars
const total = stats.totalUrls || 1;
const completedPercent = (stats.completedCount / total) * 100;
const failedPercent = (stats.failedCount / total) * 100;
const totalPercent = Math.min(100, completedPercent + failedPercent);
elements.progressPercent.textContent = `${totalPercent.toFixed(1)}%`;
elements.progressFillSuccess.style.width = `${completedPercent}%`;
elements.progressFillError.style.width = `${failedPercent}%`;
// 5. Build Scrolling Logs
if (stats.logs && stats.logs.length > 0) {
const consoleBox = elements.consoleLogs;
// Clear default loading line on first actual logs
if (consoleBox.innerHTML.includes('Sistem yükleniyor')) {
consoleBox.innerHTML = '';
}
// Find logs that are newer than what is currently rendered
const existingLinesCount = consoleBox.childElementCount;
const newLogs = stats.logs.slice(existingLinesCount);
newLogs.forEach(log => {
const line = document.createElement('div');
line.className = `console-line ${log.type}`;
const time = document.createElement('span');
time.className = 'console-time';
time.textContent = new Date(log.timestamp).toLocaleTimeString();
const msg = document.createElement('span');
msg.textContent = log.message;
line.appendChild(time);
line.appendChild(msg);
consoleBox.appendChild(line);
// Limit lines in browser DOM to 300 for memory efficiency
while (consoleBox.childElementCount > 300) {
consoleBox.removeChild(consoleBox.firstChild);
}
// Scroll to bottom
consoleBox.scrollTop = consoleBox.scrollHeight;
});
}
}
// 3. Scraper Control Actions
function initScraperControl() {
// Start/Pause Button Toggle
elements.btnToggleScrape.addEventListener('click', async () => {
const isRunningText = elements.btnToggleScrape.textContent.includes('Duraklat');
const endpoint = isRunningText ? '/api/scrape/pause' : '/api/scrape/start';
const payload = isRunningText ? {} : {
concurrency: parseInt(elements.inputConcurrency.value) || 5,
delayMs: parseInt(elements.inputDelay.value) !== undefined ? parseInt(elements.inputDelay.value) : 500
};
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.success) {
showToast(data.message);
} else {
showToast(data.error || 'Tarama durumunu değiştirme işlemi başarısız oldu.', true);
}
} catch (error) {
showToast('Sunucu ile iletişim kurulamadı.', true);
}
});
// Retry Failed
elements.btnRetryFailed.addEventListener('click', async () => {
try {
const response = await fetch('/api/scrape/retry', { method: 'POST' });
const data = await response.json();
if (data.success) {
showToast(data.message);
} else {
showToast(data.error || 'Hatalı adresleri sıfırlama işlemi başarısız.', true);
}
} catch (error) {
showToast('İşlem başarısız.', true);
}
});
// Clear Logs Console View
elements.btnClearLogs.addEventListener('click', () => {
elements.consoleLogs.innerHTML = '<div class="console-line system">Konsol temizlendi. Yeni güncellemeler bekleniyor...</div>';
});
}
// 4. Product Database Tab (Search, Filter, Pagination, Render)
function initDatabase() {
// Debounce search input
let searchTimeout = null;
elements.dbSearchInput.addEventListener('input', () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
state.dbSearch = elements.dbSearchInput.value.trim();
state.dbPage = 1;
fetchProducts();
}, 400);
});
// Select filters change
elements.filterCategory.addEventListener('change', () => {
state.dbCategory = elements.filterCategory.value;
state.dbPage = 1;
fetchProducts();
});
elements.filterCompany.addEventListener('change', () => {
state.dbCompany = elements.filterCompany.value;
state.dbPage = 1;
fetchProducts();
});
// Clear Filters
elements.btnClearFilters.addEventListener('click', () => {
elements.dbSearchInput.value = '';
elements.filterCategory.value = '';
elements.filterCompany.value = '';
state.dbSearch = '';
state.dbCategory = '';
state.dbCompany = '';
state.dbPage = 1;
fetchProducts();
});
// Pagination Click Listeners
elements.btnPrevPage.addEventListener('click', () => {
if (state.dbPage > 1) {
state.dbPage--;
fetchProducts();
}
});
elements.btnNextPage.addEventListener('click', () => {
state.dbPage++;
fetchProducts();
});
}
// Fetch products from database API
async function fetchProducts() {
elements.dbLoadingState.classList.remove('hidden');
elements.productsGrid.classList.add('hidden');
elements.dbEmptyState.classList.add('hidden');
elements.paginationControls.classList.add('hidden');
// Build URL query parameters
const params = new URLSearchParams({
page: state.dbPage,
limit: state.dbLimit,
q: state.dbSearch,
category: state.dbCategory,
company: state.dbCompany
});
try {
const response = await fetch(`/api/products?${params.toString()}`);
const data = await response.json();
elements.dbLoadingState.classList.add('hidden');
if (data.products && data.products.length > 0) {
renderProductsGrid(data.products);
renderPagination(data.pagination);
// Only update filters dropdown lists on page 1 search/initial load
if (state.dbPage === 1 && data.filters) {
populateFilters(data.filters);
}
} else {
elements.dbEmptyState.classList.remove('hidden');
}
} catch (error) {
console.error('Failed to load products:', error);
elements.dbLoadingState.classList.add('hidden');
elements.dbEmptyState.classList.remove('hidden');
showToast('Ürün veritabanı yüklenirken hata oluştu.', true);
}
}
// Populates filter select dropdown arrays
function populateFilters(filters) {
// 1. Categories dropdown
const currentCategory = elements.filterCategory.value;
elements.filterCategory.innerHTML = '<option value="">Tüm Kategoriler</option>';
filters.categories.forEach(cat => {
const option = document.createElement('option');
option.value = cat.name;
option.textContent = `${cat.name} (${cat.count})`;
if (cat.name === currentCategory) option.selected = true;
elements.filterCategory.appendChild(option);
});
// 2. Companies dropdown
const currentCompany = elements.filterCompany.value;
elements.filterCompany.innerHTML = '<option value="">Tüm Şirketler</option>';
filters.companies.forEach(comp => {
const option = document.createElement('option');
option.value = comp.name;
option.textContent = `${comp.name} (${comp.count})`;
if (comp.name === currentCompany) option.selected = true;
elements.filterCompany.appendChild(option);
});
}
// Renders the Grid of Product Cards
function renderProductsGrid(products) {
const grid = elements.productsGrid;
grid.innerHTML = '';
grid.classList.remove('hidden');
products.forEach(product => {
const card = document.createElement('div');
card.className = 'product-card';
card.dataset.id = product.id;
// Fallback placeholder image URL
const imageSrc = product.image || `https://placehold.co/400x400/1e293b/cbd5e1?text=${encodeURIComponent(product.cleanName || 'Ürün Resmi')}`;
card.innerHTML = `
<div class="product-img-wrapper">
<img src="${imageSrc}" alt="${product.cleanName || 'Ürün Görseli'}" onerror="this.src='https://placehold.co/400x400/1e293b/cbd5e1?text=%C3%9Cr%C3%BCn+G%C3%B6rseli'">
</div>
<div class="product-card-body">
<span class="product-barcode"><i class="fa-solid fa-barcode"></i> ${product.barcode}</span>
<h3 class="product-title" title="${product.name}">${product.name}</h3>
<div class="product-tags">
${product.category ? `<span class="badge badge-category"><i class="fa-solid fa-tags"></i> ${product.category}</span>` : ''}
${product.company ? `<span class="badge badge-company"><i class="fa-solid fa-building"></i> ${product.company}</span>` : ''}
${product.amountRaw ? `<span class="badge badge-qty"><i class="fa-solid fa-cubes"></i> ${product.amountRaw} ${product.unit || ''}</span>` : ''}
</div>
</div>
<div class="product-actions">
<button class="btn btn-secondary btn-small view-details-btn"><i class="fa-solid fa-eye"></i> İncele</button>
<button class="btn btn-secondary btn-small edit-product-btn" title="Düzenle"><i class="fa-solid fa-pen"></i></button>
<button class="btn btn-danger btn-small delete-product-btn" title="Sil"><i class="fa-solid fa-trash"></i></button>
</div>
`;
// Card button click bindings
card.querySelector('.view-details-btn').addEventListener('click', () => openDetailModal(product));
card.querySelector('.edit-product-btn').addEventListener('click', () => editProductForm(product));
card.querySelector('.delete-product-btn').addEventListener('click', () => deleteProduct(product));
grid.appendChild(card);
});
}
// Renders Page Numbers
function renderPagination(pageInfo) {
elements.paginationControls.classList.remove('hidden');
// Enable/Disable navigation buttons
elements.btnPrevPage.disabled = pageInfo.page <= 1;
elements.btnNextPage.disabled = pageInfo.page >= pageInfo.totalPages;
const container = elements.pageNumbersContainer;
container.innerHTML = '';
const current = pageInfo.page;
const total = pageInfo.totalPages;
// Helper logic to build numbers with dots (e.g. 1 2 ... 5 6 7 ... 9 10)
let pages = [];
if (total <= 5) {
for (let i = 1; i <= total; i++) pages.push(i);
} else {
if (current <= 3) {
pages = [1, 2, 3, 4, '...', total];
} else if (current >= total - 2) {
pages = [1, '...', total - 3, total - 2, total - 1, total];
} else {
pages = [1, '...', current - 1, current, current + 1, '...', total];
}
}
pages.forEach(p => {
const span = document.createElement('span');
if (p === '...') {
span.textContent = '...';
span.className = 'dots';
} else {
span.textContent = p;
if (p === current) {
span.className = 'active';
}
span.addEventListener('click', () => {
state.dbPage = p;
fetchProducts();
});
}
container.appendChild(span);
});
}
// Delete Product Operation
async function deleteProduct(product) {
if (!confirm(`"${product.name}" adlı ürünü silmek istediğinize emin misiniz?`)) {
return;
}
try {
const response = await fetch(`/api/products/${product.id}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showToast('Ürün başarıyla silindi.');
fetchProducts();
} else {
showToast(data.error || 'Ürün silinirken bir hata oluştu.', true);
}
} catch (error) {
showToast('İşlem gerçekleştirilemedi.', true);
}
}
// 5. Form Submissions (Add / Edit Product Manual Entry)
function initForm() {
elements.productForm.addEventListener('submit', async (e) => {
e.preventDefault();
const id = elements.formProductId.value;
const payload = {
barcode: elements.formBarcode.value.trim(),
name: elements.formName.value.trim(),
category: elements.formCategory.value.trim(),
subcategory: elements.formSubcategory.value.trim(),
company: elements.formCompany.value.trim(),
image: elements.formImage.value.trim()
};
const isEdit = id !== '';
const endpoint = isEdit ? `/api/products/${id}` : '/api/products';
const method = isEdit ? 'PUT' : 'POST';
try {
elements.btnSaveProduct.disabled = true;
elements.btnSaveProduct.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Kaydediliyor...';
const response = await fetch(endpoint, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
elements.btnSaveProduct.disabled = false;
elements.btnSaveProduct.innerHTML = '<i class="fa-solid fa-save"></i> Ürünü Kaydet';
if (response.ok) {
showToast(isEdit ? 'Ürün başarıyla güncellendi!' : 'Ürün başarıyla eklendi!');
resetForm();
// Switch to Database Tab
document.getElementById('btn-tab-database').click();
} else {
showToast(data.error || 'Kayıt başarısız oldu.', true);
}
} catch (error) {
elements.btnSaveProduct.disabled = false;
elements.btnSaveProduct.innerHTML = '<i class="fa-solid fa-save"></i> Ürünü Kaydet';
showToast('Sunucu bağlantı hatası.', true);
}
});
elements.btnResetForm.addEventListener('click', resetForm);
}
// Redirects and fills Form for Edit Action
function editProductForm(product) {
// Fill the inputs
elements.formProductId.value = product.id;
elements.formBarcode.value = product.barcode;
elements.formName.value = product.name;
elements.formCategory.value = product.category || '';
elements.formSubcategory.value = product.subcategory || '';
elements.formCompany.value = product.company || '';
elements.formImage.value = product.image || '';
// Update labels and display
document.getElementById('btn-tab-add').click();
elements.pageTitle.textContent = 'Ürün Düzenle';
elements.pageSubtitle.textContent = `"${product.cleanName}" barkod kaydını güncelleyin`;
elements.btnSaveProduct.innerHTML = '<i class="fa-solid fa-check"></i> Değişiklikleri Kaydet';
}
function resetForm() {
elements.productForm.reset();
elements.formProductId.value = '';
elements.btnSaveProduct.innerHTML = '<i class="fa-solid fa-save"></i> Ürünü Kaydet';
if (state.currentTab === 'add-product-tab') {
elements.pageTitle.textContent = 'Ürün Ekle';
elements.pageSubtitle.textContent = 'Veritabanına manuel olarak yeni bir barkod kaydı girin';
}
}
// 6. Product Detail Modal & Barcode Renders
function initModal() {
elements.btnCloseModal.addEventListener('click', () => {
elements.productDetailModal.classList.remove('active');
state.selectedProduct = null;
});
// Close on overlay overlay click
elements.productDetailModal.addEventListener('click', (e) => {
if (e.target === elements.productDetailModal) {
elements.productDetailModal.classList.remove('active');
state.selectedProduct = null;
}
});
// Handle code downloads (SVG for Barcode, Canvas for QR Code)
elements.downloadButtons.forEach(btn => {
btn.addEventListener('click', () => {
const type = btn.getAttribute('data-type');
if (!state.selectedProduct) return;
const barcodeVal = state.selectedProduct.barcode;
const productName = state.selectedProduct.cleanName || 'barkod';
if (type === 'barcode') {
// EAN-13 SVG Download
const svgSerializer = new XMLSerializer();
const svgString = svgSerializer.serializeToString(elements.modalBarcodeSvg);
const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = `EAN13_${barcodeVal}_${productName.toLowerCase().replace(/[^a-z0-9]/g, '_')}.svg`;
a.click();
URL.revokeObjectURL(blobUrl);
} else if (type === 'qrcode') {
// QR Code PNG Download
const canvas = elements.modalQrcodeCanvas;
const dataUrl = canvas.toDataURL('image/png');
const a = document.createElement('a');
a.href = dataUrl;
a.download = `QR_${barcodeVal}_${productName.toLowerCase().replace(/[^a-z0-9]/g, '_')}.png`;
a.click();
}
});
});
}
function openDetailModal(product) {
state.selectedProduct = product;
// Fill text labels
elements.modalProductName.textContent = product.name;
elements.modalCleanName.textContent = product.cleanName || '-';
elements.modalAmountRaw.textContent = product.amountRaw || 'Miktar Belirtilmemiş';
elements.modalAmount.textContent = product.amount !== null ? product.amount : '-';
elements.modalUnit.textContent = product.unit || '-';
// Badges
elements.modalProductCategory.innerHTML = product.category
? `<i class="fa-solid fa-tags"></i> ${product.category}`
: '<i class="fa-solid fa-tags"></i> Kategorisiz';
elements.modalProductCompany.innerHTML = product.company
? `<i class="fa-solid fa-building"></i> ${product.company}`
: '<i class="fa-solid fa-building"></i> Şirket Belirsiz';
// Product Image
const imageSrc = product.image || `https://placehold.co/400x400/1e293b/cbd5e1?text=${encodeURIComponent(product.cleanName)}`;
elements.modalProductImage.src = imageSrc;
// GENERATE BARKOD (EAN-13 or Code128)
const barcodeVal = product.barcode;
try {
// EAN-13 requires exactly 12 or 13 numerical digits.
// If it's something else, we fallback to CODE128 format so it successfully renders.
const isNumericOnly = /^\d+$/.test(barcodeVal);
const format = (isNumericOnly && (barcodeVal.length === 12 || barcodeVal.length === 13)) ? "EAN13" : "CODE128";
JsBarcode("#modal-barcode-svg", barcodeVal, {
format: format,
width: 2,
height: 90,
displayValue: true,
fontSize: 14,
font: 'var(--font-mono)',
background: "#ffffff",
lineColor: "#000000",
margin: 10
});
} catch (err) {
console.error('Barcode generation error:', err);
elements.modalBarcodeSvg.innerHTML = `<text x="10" y="50" fill="red">Barkod Oluşturulamadı (${err.message})</text>`;
}
// GENERATE QR CODE
try {
QRCode.toCanvas(elements.modalQrcodeCanvas, barcodeVal, {
width: 150,
margin: 1,
color: {
dark: "#000000",
light: "#ffffff"
}
}, function (error) {
if (error) console.error(error);
});
} catch (err) {
console.error('QR Code generation error:', err);
}
// Toggle Modal class
elements.productDetailModal.classList.add('active');
}

357
public/index.html Normal file
View File

@@ -0,0 +1,357 @@
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hBarkodDB | Akıllı Barkod Veritabanı ve Tarayıcı</title>
<meta name="description" content="Barkodist verilerini işleyip barkod ve ürün veritabanı oluşturan akıllı otomasyon sistemi.">
<link rel="stylesheet" href="style.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Plus+Jakarta+Sans:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
<!-- FontAwesome for Premium Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- CDNs for Barcode and QR Code Generation -->
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<!-- Frontend logic -->
<script src="app.js" defer></script>
</head>
<body>
<div class="app-container">
<!-- Sidebar Navigation -->
<aside class="sidebar">
<div class="logo-area">
<div class="logo-icon">
<i class="fa-solid fa-barcode-read"></i>
</div>
<div class="logo-text">
<span class="brand-name">hBarkod<span class="accent-text">DB</span></span>
<span class="brand-sub">Veri & Barkod Sistemi</span>
</div>
</div>
<nav class="nav-menu">
<button class="nav-btn active" data-tab="dashboard-tab" id="btn-tab-dashboard">
<i class="fa-solid fa-chart-line"></i> <span>Kontrol Paneli</span>
</button>
<button class="nav-btn" data-tab="database-tab" id="btn-tab-database">
<i class="fa-solid fa-database"></i> <span>Ürün Veritabanı</span>
</button>
<button class="nav-btn" data-tab="add-product-tab" id="btn-tab-add">
<i class="fa-solid fa-plus-circle"></i> <span>Ürün Ekle</span>
</button>
</nav>
<div class="sidebar-footer">
<div class="status-indicator connected">
<span class="dot"></span>
<span class="status-text">Sunucu Bağlantısı Aktif</span>
</div>
</div>
</aside>
<!-- Main Content Area -->
<main class="main-content">
<!-- Header -->
<header class="app-header">
<div class="header-title">
<h1 id="page-title">Kontrol Paneli</h1>
<p class="subtitle" id="page-subtitle">Veri tarama sürecini ve sunucu istatistiklerini canlı takip edin</p>
</div>
<div class="header-actions">
<div class="api-speed">
<i class="fa-solid fa-bolt text-yellow"></i>
<span id="active-threads-badge">0 Aktif İşçi</span>
</div>
</div>
</header>
<!-- Tab: Dashboard -->
<section id="dashboard-tab" class="tab-content active">
<!-- Stat Grid -->
<div class="stats-grid">
<div class="stat-card total">
<div class="stat-icon"><i class="fa-solid fa-link"></i></div>
<div class="stat-info">
<h3>Toplam Adres</h3>
<div class="value" id="stat-total">0</div>
</div>
</div>
<div class="stat-card completed">
<div class="stat-icon"><i class="fa-solid fa-check-double"></i></div>
<div class="stat-info">
<h3>Taranan Ürün</h3>
<div class="value" id="stat-completed">0</div>
</div>
</div>
<div class="stat-card pending">
<div class="stat-icon"><i class="fa-solid fa-clock-rotate-left"></i></div>
<div class="stat-info">
<h3>Sırada Bekleyen</h3>
<div class="value" id="stat-pending">0</div>
</div>
</div>
<div class="stat-card failed">
<div class="stat-icon"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div class="stat-info">
<h3>Hatalı Deneme</h3>
<div class="value" id="stat-failed">0</div>
</div>
</div>
</div>
<!-- Progress Section -->
<div class="card progress-card">
<div class="card-header">
<h2>Tarama İlerleme Durumu</h2>
<span class="percentage" id="progress-percent">0%</span>
</div>
<div class="progress-bar-wrapper">
<div class="progress-bar">
<div class="progress-fill fill-success" id="progress-fill-success" style="width: 0%"></div>
<div class="progress-fill fill-error" id="progress-fill-error" style="width: 0%"></div>
</div>
</div>
</div>
<!-- Control and Logs Area -->
<div class="dashboard-split">
<!-- Control Settings Card -->
<div class="card control-card">
<h2>Tarayıcı Kontrolü</h2>
<p class="card-description">Eşzamanlı istek sayısını ve gecikmeyi ayarlayarak tarama işlemini yönetin.</p>
<div class="control-group">
<label for="input-concurrency">Eşzamanlı İşçi (Thread) Sayısı</label>
<div class="number-input-wrapper">
<input type="number" id="input-concurrency" min="1" max="50" value="5">
<span class="input-hint">Önerilen: 5-10</span>
</div>
</div>
<div class="control-group">
<label for="input-delay">İstekler Arası Gecikme (ms)</label>
<div class="number-input-wrapper">
<input type="number" id="input-delay" min="0" max="10000" step="100" value="500">
<span class="input-hint">Sayfayı yormamak için ideal: 500ms</span>
</div>
</div>
<div class="control-actions">
<button id="btn-toggle-scrape" class="btn btn-primary btn-large">
<i class="fa-solid fa-play"></i> Tarayıcıyı Başlat
</button>
<button id="btn-retry-failed" class="btn btn-secondary">
<i class="fa-solid fa-rotate-left"></i> Hatalıları Yeniden Dene
</button>
</div>
</div>
<!-- Live Logs Card -->
<div class="card logs-card">
<div class="card-header">
<h2>Canlı Tarama Günlüğü</h2>
<button id="btn-clear-logs" class="btn btn-text-icon" title="Konsolu Temizle">
<i class="fa-solid fa-trash-can"></i>
</button>
</div>
<div class="console-box" id="console-logs">
<div class="console-line system">Sistem yükleniyor. Bağlantı bekleniyor...</div>
</div>
</div>
</div>
</section>
<!-- Tab: Product Database -->
<section id="database-tab" class="tab-content">
<!-- Search & Filters -->
<div class="card filter-card">
<div class="filter-row">
<div class="search-box">
<i class="fa-solid fa-magnifying-glass"></i>
<input type="text" id="db-search" placeholder="Ürün adı, barkod, şirket veya kategori ara...">
</div>
<div class="filter-selects">
<select id="filter-category">
<option value="">Tüm Kategoriler</option>
</select>
<select id="filter-company">
<option value="">Tüm Şirketler</option>
</select>
<button id="btn-clear-filters" class="btn btn-secondary" title="Filtreleri Sıfırla">
<i class="fa-solid fa-filter-circle-xmark"></i> Temizle
</button>
</div>
</div>
</div>
<!-- Products Display -->
<div class="products-container">
<div class="products-grid" id="products-grid-list">
<!-- Product cards will be injected here -->
</div>
<!-- Empty State -->
<div class="empty-state hidden" id="db-empty-state">
<div class="empty-icon"><i class="fa-solid fa-box-open"></i></div>
<h2>Ürün Bulunamadı</h2>
<p>Aradığınız kriterlere uygun herhangi bir barkod kaydı mevcut değil.</p>
</div>
<!-- Loading State -->
<div class="loading-state hidden" id="db-loading-state">
<div class="spinner"></div>
<p>Ürünler yükleniyor...</p>
</div>
<!-- Pagination -->
<div class="pagination-wrapper" id="pagination-controls">
<button class="btn btn-page" id="btn-prev-page"><i class="fa-solid fa-chevron-left"></i> Önceki</button>
<div class="page-numbers" id="page-numbers-container">
<span class="active">1</span>
</div>
<button class="btn btn-page" id="btn-next-page">Sonraki <i class="fa-solid fa-chevron-right"></i></button>
</div>
</div>
</section>
<!-- Tab: Add Product -->
<section id="add-product-tab" class="tab-content">
<div class="card form-card">
<h2>Manuel Barkod & Ürün Ekle</h2>
<p class="card-description">Veritabanına manuel olarak yeni bir ürün ve barkod kaydı girmek için aşağıdaki formu kullanın.</p>
<form id="product-form" class="premium-form">
<input type="hidden" id="form-product-id" value="">
<div class="form-row">
<div class="form-group">
<label for="form-barcode">Barkod Numarası (EAN-13) *</label>
<input type="text" id="form-barcode" required placeholder="Örn: 8680908020014">
</div>
<div class="form-group">
<label for="form-name">Ürün Adı (Miktar ve Birimli) *</label>
<input type="text" id="form-name" required placeholder="Örn: Indomie Körili Hazır Noodle (75 g)">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="form-category">Kategori</label>
<input type="text" id="form-category" placeholder="Örn: Gıda Ürünleri">
</div>
<div class="form-group">
<label for="form-subcategory">Alt Kategori</label>
<input type="text" id="form-subcategory" placeholder="Örn: Makarna">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="form-company">Ait Olduğu Şirket</label>
<input type="text" id="form-company" placeholder="Örn: Indofood">
</div>
<div class="form-group">
<label for="form-image">Ürün Görsel URL'si</label>
<input type="text" id="form-image" placeholder="Örn: https://example.com/image.jpg">
</div>
</div>
<div class="form-actions">
<button type="submit" id="btn-save-product" class="btn btn-primary btn-large">
<i class="fa-solid fa-save"></i> Ürünü Kaydet
</button>
<button type="button" id="btn-reset-form" class="btn btn-secondary">
Temizle
</button>
</div>
</form>
</div>
</section>
</main>
</div>
<!-- Product Detail & Barcode Generator Modal -->
<div class="modal-overlay" id="product-detail-modal">
<div class="modal-container">
<button class="modal-close" id="btn-close-modal"><i class="fa-solid fa-xmark"></i></button>
<div class="modal-content-grid">
<!-- Left: Product info and Image -->
<div class="modal-left">
<div class="modal-product-img-box">
<img id="modal-product-image" src="" alt="Ürün Resmi" onerror="this.src='https://placehold.co/400x400/1e293b/cbd5e1?text=%C3%9Cr%C3%BCn+G%C3%B6rseli'">
</div>
<div class="modal-product-meta">
<h2 id="modal-product-name">Ürün Adı Buraya Gelecek</h2>
<div class="tag-row">
<span class="badge badge-category" id="modal-product-category"><i class="fa-solid fa-tags"></i> Kategori</span>
<span class="badge badge-company" id="modal-product-company"><i class="fa-solid fa-building"></i> Şirket</span>
</div>
<div class="parsed-details">
<h3>Birim ve Miktar Analizi</h3>
<table class="details-table">
<tr>
<td>Temiz Ürün Adı</td>
<td id="modal-clean-name">-</td>
</tr>
<tr>
<td>Miktar (Ham)</td>
<td id="modal-amount-raw">-</td>
</tr>
<tr>
<td>Sayısal Miktar</td>
<td id="modal-amount">-</td>
</tr>
<tr>
<td>Birim</td>
<td id="modal-unit">-</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Right: Barcode & QR Code generators -->
<div class="modal-right">
<h2>Barkod & Kod Üretimi</h2>
<p class="modal-desc">Ürün kodunu anında EAN-13 standartlarında veya mobil uyumlu QR kod olarak üretebilir, indirebilirsiniz.</p>
<div class="code-generator-section">
<!-- EAN-13 Barcode -->
<div class="barcode-render-box">
<div class="render-title">EAN-13 Barkodu</div>
<div class="canvas-wrapper">
<svg id="modal-barcode-svg"></svg>
</div>
<button class="btn btn-small btn-secondary download-btn" data-type="barcode">
<i class="fa-solid fa-download"></i> Barkod İndir (SVG)
</button>
</div>
<!-- QR Code -->
<div class="barcode-render-box">
<div class="render-title">QR Kod</div>
<div class="canvas-wrapper">
<canvas id="modal-qrcode-canvas"></canvas>
</div>
<button class="btn btn-small btn-secondary download-btn" data-type="qrcode">
<i class="fa-solid fa-download"></i> QR Kod İndir (PNG)
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Notification Toast -->
<div class="toast" id="toast">
<i class="fa-solid fa-circle-check toast-icon"></i>
<span class="toast-message">İşlem başarıyla gerçekleştirildi.</span>
</div>
</body>
</html>

1230
public/style.css Normal file

File diff suppressed because it is too large Load Diff