İlk sürüm: hDiyanetProxy v1.0.0
- Backend: Node.js + Express + MySQL + JWT auth - 8 MySQL tablosu (users, countries, states, cities, prayer_times, ramadan_times, eid_times, fetch_logs) - Diyanet API entegrasyonu (auth + token yönetimi) - Tüm API endpointleri (places, prayer-times, ramadan, eid, admin) - Rate limiting, CORS, input validation - Cron job (gece 02:00 otomatik veri çekme) - Frontend: Login, Dashboard, Fetch Panel, Namaz Vakitleri, Ramazan, Admin, Profil - Admin kullanıcı: admin/admin123
This commit is contained in:
75
frontend/src/components/sidebar.js
Normal file
75
frontend/src/components/sidebar.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// hDiyanetProxy - Sidebar Bileşeni
|
||||
function renderSidebar(activePage) {
|
||||
const user = getUser();
|
||||
const isAdmin = user && user.role === 'admin';
|
||||
|
||||
return `
|
||||
<button class="mobile-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">☰</button>
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>🕌 hDiyanetProxy</h2>
|
||||
<small>Namaz Vakitleri Proxy API</small>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">Genel</div>
|
||||
<a href="/dashboard" class="${activePage === 'dashboard' ? 'active' : ''}">📊 Dashboard</a>
|
||||
<a href="/prayer-times" class="${activePage === 'prayer-times' ? 'active' : ''}">🕐 Namaz Vakitleri</a>
|
||||
<a href="/ramadan" class="${activePage === 'ramadan' ? 'active' : ''}">🌙 Ramazan / İftar</a>
|
||||
${isAdmin ? `
|
||||
<div class="nav-section">Yönetim</div>
|
||||
<a href="/fetch" class="${activePage === 'fetch' ? 'active' : ''}">📥 Veri Çekme</a>
|
||||
<a href="/admin/users" class="${activePage === 'admin-users' ? 'active' : ''}">👥 Kullanıcılar</a>
|
||||
` : ''}
|
||||
<div class="nav-section">Hesap</div>
|
||||
<a href="/profile" class="${activePage === 'profile' ? 'active' : ''}">👤 Profil & API</a>
|
||||
<a href="#" onclick="logout(); return false;">🚪 Çıkış Yap</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-info">
|
||||
<span>👤 ${user ? user.username : 'Misafir'}</span>
|
||||
${user ? `<span class="user-badge">${user.role}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
`;
|
||||
}
|
||||
|
||||
// Ülke-Eyalet-Şehir dropdown zinciri yükleme
|
||||
async function loadCountryDropdown(selectId) {
|
||||
try {
|
||||
const countries = await apiFetch('/places/countries');
|
||||
const select = document.getElementById(selectId);
|
||||
select.innerHTML = '<option value="">Ülke seçin...</option>';
|
||||
countries.forEach(c => {
|
||||
select.innerHTML += `<option value="${c.id}">${c.name}</option>`;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Ülkeler yüklenemedi:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStateDropdown(countryId, selectId) {
|
||||
try {
|
||||
const states = await apiFetch(`/places/states?countryId=${countryId}`);
|
||||
const select = document.getElementById(selectId);
|
||||
select.innerHTML = '<option value="">İl/Eyalet seçin...</option>';
|
||||
states.forEach(s => {
|
||||
select.innerHTML += `<option value="${s.id}">${s.name}</option>`;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Eyaletler yüklenemedi:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCityDropdown(stateId, selectId) {
|
||||
try {
|
||||
const cities = await apiFetch(`/places/cities?stateId=${stateId}`);
|
||||
const select = document.getElementById(selectId);
|
||||
select.innerHTML = '<option value="">Şehir/İlçe seçin...</option>';
|
||||
cities.forEach(c => {
|
||||
select.innerHTML += `<option value="${c.id}">${c.name}</option>`;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Şehirler yüklenemedi:', err);
|
||||
}
|
||||
}
|
||||
157
frontend/src/pages/admin-users.html
Normal file
157
frontend/src/pages/admin-users.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kullanıcı Yönetimi - hDiyanetProxy</title>
|
||||
<meta name="description" content="Admin kullanıcı yönetim paneli">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout" id="app"></div>
|
||||
|
||||
<script src="/services/api.js"></script>
|
||||
<script src="/components/sidebar.js"></script>
|
||||
<script>
|
||||
if (!requireAuth()) throw new Error('Auth');
|
||||
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = renderSidebar('admin-users') + `
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<h1>👥 Kullanıcı Yönetimi</h1>
|
||||
<p>Kullanıcıları listeleyin, ekleyin ve yönetin</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Yeni Kullanıcı Ekle</h3>
|
||||
</div>
|
||||
<form onsubmit="createUser(event)" style="display:flex; gap:12px; flex-wrap:wrap; align-items:end;">
|
||||
<div class="form-group" style="margin-bottom:0;">
|
||||
<label for="newUsername">Kullanıcı Adı</label>
|
||||
<input type="text" id="newUsername" class="form-control" required minlength="3" maxlength="50" placeholder="kullanici_adi">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0;">
|
||||
<label for="newPassword">Şifre</label>
|
||||
<input type="password" id="newPassword" class="form-control" required minlength="6" placeholder="••••••">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0;">
|
||||
<label for="newRole">Rol</label>
|
||||
<select id="newRole" class="form-control">
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" id="btnCreate">➕ Ekle</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Kullanıcı Listesi</h3>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Kullanıcı Adı</th>
|
||||
<th>Rol</th>
|
||||
<th>Durum</th>
|
||||
<th>Oluşturulma</th>
|
||||
<th>İşlemler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersBody">
|
||||
<tr><td colspan="6" style="text-align:center; padding:20px;">Yükleniyor...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
loadUsers();
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const users = await apiFetch('/admin/users');
|
||||
const body = document.getElementById('usersBody');
|
||||
const currentUser = getUser();
|
||||
|
||||
body.innerHTML = users.map(u => `
|
||||
<tr>
|
||||
<td>${u.id}</td>
|
||||
<td><strong>${u.username}</strong></td>
|
||||
<td><span class="badge badge-${u.role === 'admin' ? 'admin' : 'user'}">${u.role}</span></td>
|
||||
<td><span class="badge badge-${u.is_active ? 'success' : 'error'}">${u.is_active ? 'Aktif' : 'Pasif'}</span></td>
|
||||
<td>${formatDateTime(u.created_at)}</td>
|
||||
<td>
|
||||
${u.id !== currentUser.id ? `
|
||||
<button class="btn btn-outline btn-sm" onclick="toggleUser(${u.id}, ${!u.is_active})">
|
||||
${u.is_active ? '⏸ Pasif Yap' : '▶️ Aktif Yap'}
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteUser(${u.id}, '${u.username}')">🗑️</button>
|
||||
` : '<span style="color:var(--text-lighter); font-size:12px;">Mevcut Kullanıcı</span>'}
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function createUser(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('btnCreate');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
await apiFetch('/admin/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('newUsername').value,
|
||||
password: document.getElementById('newPassword').value,
|
||||
role: document.getElementById('newRole').value
|
||||
})
|
||||
});
|
||||
showToast('Kullanıcı oluşturuldu!', 'success');
|
||||
document.getElementById('newUsername').value = '';
|
||||
document.getElementById('newPassword').value = '';
|
||||
loadUsers();
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleUser(id, isActive) {
|
||||
try {
|
||||
await apiFetch(`/admin/users/${id}/toggle`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ isActive })
|
||||
});
|
||||
showToast(isActive ? 'Kullanıcı aktif edildi' : 'Kullanıcı pasif edildi', 'success');
|
||||
loadUsers();
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(id, username) {
|
||||
if (!confirm(`"${username}" kullanıcısını silmek istediğinize emin misiniz?`)) return;
|
||||
|
||||
try {
|
||||
await apiFetch(`/admin/users/${id}`, { method: 'DELETE' });
|
||||
showToast('Kullanıcı silindi', 'success');
|
||||
loadUsers();
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
97
frontend/src/pages/dashboard.html
Normal file
97
frontend/src/pages/dashboard.html
Normal file
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard - hDiyanetProxy</title>
|
||||
<meta name="description" content="hDiyanetProxy Dashboard - Sistem genel durumu ve istatistikler">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout" id="app"></div>
|
||||
|
||||
<script src="/services/api.js"></script>
|
||||
<script src="/components/sidebar.js"></script>
|
||||
<script>
|
||||
if (!requireAuth()) throw new Error('Auth');
|
||||
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = renderSidebar('dashboard') + `
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<h1>📊 Dashboard</h1>
|
||||
<p>Sistem genel durumu ve istatistikler</p>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid" id="statsGrid">
|
||||
<div class="stat-card"><div class="stat-icon">⏳</div><div class="stat-value">-</div><div class="stat-label">Yükleniyor...</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📋 Son Veri Çekme Logları</h3>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tarih</th>
|
||||
<th>Kullanıcı</th>
|
||||
<th>İşlem</th>
|
||||
<th>Şehir</th>
|
||||
<th>Durum</th>
|
||||
<th>Kayıt</th>
|
||||
<th>Detay</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="logsBody">
|
||||
<tr><td colspan="7" style="text-align:center; padding:20px;">Yükleniyor...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
loadDashboard();
|
||||
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
const data = await apiFetch('/admin/dashboard');
|
||||
|
||||
// İstatistikler
|
||||
const s = data.stats;
|
||||
document.getElementById('statsGrid').innerHTML = `
|
||||
<div class="stat-card"><div class="stat-icon">🌍</div><div class="stat-value">${s.countries}</div><div class="stat-label">Ülke</div></div>
|
||||
<div class="stat-card"><div class="stat-icon">🏙️</div><div class="stat-value">${s.states}</div><div class="stat-label">İl/Eyalet</div></div>
|
||||
<div class="stat-card"><div class="stat-icon">📍</div><div class="stat-value">${s.cities}</div><div class="stat-label">Şehir/İlçe</div></div>
|
||||
<div class="stat-card"><div class="stat-icon">🕐</div><div class="stat-value">${s.prayerTimes}</div><div class="stat-label">Namaz Vakti</div></div>
|
||||
<div class="stat-card"><div class="stat-icon">🌙</div><div class="stat-value">${s.ramadanTimes}</div><div class="stat-label">Ramazan Kaydı</div></div>
|
||||
<div class="stat-card"><div class="stat-icon">🎉</div><div class="stat-value">${s.eidTimes}</div><div class="stat-label">Bayram Kaydı</div></div>
|
||||
`;
|
||||
|
||||
// Loglar
|
||||
const logsBody = document.getElementById('logsBody');
|
||||
if (data.recentLogs.length === 0) {
|
||||
logsBody.innerHTML = '<tr><td colspan="7" style="text-align:center; padding:20px; color:var(--text-light);">Henüz log kaydı yok</td></tr>';
|
||||
} else {
|
||||
logsBody.innerHTML = data.recentLogs.map(log => `
|
||||
<tr>
|
||||
<td>${formatDateTime(log.created_at)}</td>
|
||||
<td>${log.username || 'Sistem'}</td>
|
||||
<td><span class="badge badge-info">${log.action}</span></td>
|
||||
<td>${log.city_name || '-'}</td>
|
||||
<td><span class="badge badge-${log.status === 'success' ? 'success' : 'error'}">${log.status}</span></td>
|
||||
<td>${log.records_count}</td>
|
||||
<td style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${log.details || ''}">${log.details || '-'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
237
frontend/src/pages/fetch.html
Normal file
237
frontend/src/pages/fetch.html
Normal file
@@ -0,0 +1,237 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Veri Çekme - hDiyanetProxy</title>
|
||||
<meta name="description" content="Diyanet API'den veri çekme paneli">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout" id="app"></div>
|
||||
|
||||
<script src="/services/api.js"></script>
|
||||
<script src="/components/sidebar.js"></script>
|
||||
<script>
|
||||
if (!requireAuth()) throw new Error('Auth');
|
||||
|
||||
const app = document.getElementById('app');
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
app.innerHTML = renderSidebar('fetch') + `
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<h1>📥 Veri Çekme Paneli</h1>
|
||||
<p>Diyanet API'den veri çekip veritabanına kaydedin</p>
|
||||
</div>
|
||||
|
||||
<!-- Yer Bilgileri Çekme -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📍 Yer Bilgileri</h3>
|
||||
<button class="btn btn-primary btn-sm" onclick="fetchPlaces()" id="btnFetchPlaces">
|
||||
📥 Tüm Yer Bilgilerini Çek
|
||||
</button>
|
||||
</div>
|
||||
<p style="color:var(--text-light); font-size:13px;">Ülke, eyalet ve şehir bilgilerini Diyanet API'den çeker. İlk kurulumda ve periyodik olarak çalıştırılmalıdır.</p>
|
||||
</div>
|
||||
|
||||
<!-- Şehir Seçimi -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>🏙️ Şehir Seçimi</h3>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="countrySelect">Ülke</label>
|
||||
<select id="countrySelect" class="form-control" onchange="loadStateDropdown(this.value, 'stateSelect'); document.getElementById('citySelect').innerHTML='<option value=\\'\\'>Önce il seçin...</option>';">
|
||||
<option value="">Yükleniyor...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="stateSelect">İl / Eyalet</label>
|
||||
<select id="stateSelect" class="form-control" onchange="loadCityDropdown(this.value, 'citySelect')">
|
||||
<option value="">Önce ülke seçin...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="citySelect">Şehir / İlçe</label>
|
||||
<select id="citySelect" class="form-control">
|
||||
<option value="">Önce il seçin...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Veri Çekme İşlemleri -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>🕌 Veri Çekme İşlemleri</h3>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:16px;">
|
||||
<div class="form-group">
|
||||
<label for="startDate">Başlangıç Tarihi</label>
|
||||
<input type="date" id="startDate" class="form-control" value="${currentYear}-01-01">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="endDate">Bitiş Tarihi</label>
|
||||
<input type="date" id="endDate" class="form-control" value="${currentYear}-12-31">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:10px; flex-wrap:wrap;">
|
||||
<button class="btn btn-primary" onclick="fetchYearly()" id="btnYearly">🕐 Yıllık Namaz Vakitlerini Çek</button>
|
||||
<button class="btn btn-accent" onclick="fetchRamadan()" id="btnRamadan">🌙 Ramazan Verilerini Çek</button>
|
||||
<button class="btn btn-primary" onclick="fetchEid()" id="btnEid">🎉 Bayram Verilerini Çek</button>
|
||||
<button class="btn btn-outline" onclick="clearDiyanetCache()" id="btnCache">🗑️ Diyanet Cache Temizle</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- İşlem Konsolu -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📟 İşlem Konsolu</h3>
|
||||
<button class="btn btn-outline btn-sm" onclick="document.getElementById('logConsole').textContent = '> Konsol temizlendi\\n'">Temizle</button>
|
||||
</div>
|
||||
<div class="log-console" id="logConsole">> Hazır. Bir işlem seçin ve başlatın.\n</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
// Sayfa yüklendiğinde ülkeleri yükle
|
||||
loadCountryDropdown('countrySelect');
|
||||
|
||||
function addLog(msg, type = 'info') {
|
||||
const el = document.getElementById('logConsole');
|
||||
const span = document.createElement('span');
|
||||
span.className = `log-${type}`;
|
||||
span.textContent = `> ${msg}\n`;
|
||||
el.appendChild(span);
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
|
||||
function getSelectedCityId() {
|
||||
const cityId = document.getElementById('citySelect').value;
|
||||
if (!cityId) {
|
||||
showToast('Lütfen bir şehir seçin', 'warning');
|
||||
return null;
|
||||
}
|
||||
return cityId;
|
||||
}
|
||||
|
||||
async function fetchPlaces() {
|
||||
const btn = document.getElementById('btnFetchPlaces');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Çekiliyor...';
|
||||
addLog('Yer bilgileri çekiliyor... (Bu işlem birkaç dakika sürebilir)', 'info');
|
||||
|
||||
try {
|
||||
const result = await apiFetch('/admin/fetch/places', { method: 'POST' });
|
||||
addLog(`✅ ${result.message} (${result.totalRecords} kayıt)`, 'success');
|
||||
showToast('Yer bilgileri güncellendi!', 'success');
|
||||
loadCountryDropdown('countrySelect');
|
||||
} catch (err) {
|
||||
addLog(`❌ Hata: ${err.message}`, 'error');
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '📥 Tüm Yer Bilgilerini Çek';
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchYearly() {
|
||||
const cityId = getSelectedCityId();
|
||||
if (!cityId) return;
|
||||
|
||||
const startDate = document.getElementById('startDate').value + 'T00:00:00.0Z';
|
||||
const endDate = document.getElementById('endDate').value + 'T23:59:59.0Z';
|
||||
const btn = document.getElementById('btnYearly');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Çekiliyor...';
|
||||
addLog(`Namaz vakitleri çekiliyor: cityId=${cityId}`, 'info');
|
||||
|
||||
try {
|
||||
const result = await apiFetch('/admin/fetch/yearly', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ cityId: parseInt(cityId), startDate, endDate })
|
||||
});
|
||||
addLog(`✅ ${result.message}`, 'success');
|
||||
showToast(result.message, 'success');
|
||||
} catch (err) {
|
||||
addLog(`❌ Hata: ${err.message}`, 'error');
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🕐 Yıllık Namaz Vakitlerini Çek';
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRamadan() {
|
||||
const cityId = getSelectedCityId();
|
||||
if (!cityId) return;
|
||||
|
||||
const btn = document.getElementById('btnRamadan');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Çekiliyor...';
|
||||
addLog(`Ramazan vakitleri çekiliyor: cityId=${cityId}`, 'info');
|
||||
|
||||
try {
|
||||
const result = await apiFetch('/admin/fetch/ramadan', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ cityId: parseInt(cityId) })
|
||||
});
|
||||
addLog(`✅ ${result.message}`, 'success');
|
||||
showToast(result.message, 'success');
|
||||
} catch (err) {
|
||||
addLog(`❌ Hata: ${err.message}`, 'error');
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🌙 Ramazan Verilerini Çek';
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEid() {
|
||||
const cityId = getSelectedCityId();
|
||||
if (!cityId) return;
|
||||
|
||||
const btn = document.getElementById('btnEid');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Çekiliyor...';
|
||||
addLog(`Bayram vakitleri çekiliyor: cityId=${cityId}`, 'info');
|
||||
|
||||
try {
|
||||
const result = await apiFetch('/admin/fetch/eid', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ cityId: parseInt(cityId) })
|
||||
});
|
||||
addLog(`✅ ${result.message}`, 'success');
|
||||
showToast(result.message, 'success');
|
||||
} catch (err) {
|
||||
addLog(`❌ Hata: ${err.message}`, 'error');
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🎉 Bayram Verilerini Çek';
|
||||
}
|
||||
}
|
||||
|
||||
async function clearDiyanetCache() {
|
||||
const btn = document.getElementById('btnCache');
|
||||
btn.disabled = true;
|
||||
addLog('Diyanet API cache temizleniyor...', 'info');
|
||||
|
||||
try {
|
||||
await apiFetch('/admin/cache/clear', { method: 'POST' });
|
||||
addLog('✅ Cache temizlendi', 'success');
|
||||
showToast('Cache temizlendi', 'success');
|
||||
} catch (err) {
|
||||
addLog(`❌ Hata: ${err.message}`, 'error');
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
71
frontend/src/pages/login.html
Normal file
71
frontend/src/pages/login.html
Normal file
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Giriş - hDiyanetProxy</title>
|
||||
<meta name="description" content="hDiyanetProxy - Namaz Vakitleri Proxy API Giriş">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-page">
|
||||
<div class="login-card">
|
||||
<h1>🕌 hDiyanetProxy</h1>
|
||||
<p class="subtitle">Namaz Vakitleri Proxy API</p>
|
||||
|
||||
<div id="loginError" class="login-error"></div>
|
||||
|
||||
<form id="loginForm" onsubmit="handleLogin(event)">
|
||||
<div class="form-group">
|
||||
<label for="username">Kullanıcı Adı</label>
|
||||
<input type="text" id="username" class="form-control" placeholder="admin" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Şifre</label>
|
||||
<input type="password" id="password" class="form-control" placeholder="••••••" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" id="loginBtn">Giriş Yap</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/services/api.js"></script>
|
||||
<script>
|
||||
// Zaten giriş yapılmışsa dashboard'a yönlendir
|
||||
if (getToken()) {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('loginBtn');
|
||||
const errorEl = document.getElementById('loginError');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Giriş yapılıyor...';
|
||||
errorEl.style.display = 'none';
|
||||
|
||||
try {
|
||||
const data = await apiFetch('/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
})
|
||||
});
|
||||
|
||||
setToken(data.token);
|
||||
setUser(data.user);
|
||||
window.location.href = '/dashboard';
|
||||
} catch (err) {
|
||||
errorEl.textContent = err.message;
|
||||
errorEl.style.display = 'block';
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Giriş Yap';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
157
frontend/src/pages/prayer-times.html
Normal file
157
frontend/src/pages/prayer-times.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Namaz Vakitleri - hDiyanetProxy</title>
|
||||
<meta name="description" content="Namaz vakitleri sorgulama ve listeleme">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout" id="app"></div>
|
||||
|
||||
<script src="/services/api.js"></script>
|
||||
<script src="/components/sidebar.js"></script>
|
||||
<script>
|
||||
if (!requireAuth()) throw new Error('Auth');
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
let currentData = [];
|
||||
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = renderSidebar('prayer-times') + `
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<h1>🕐 Namaz Vakitleri</h1>
|
||||
<p>Şehir ve tarih aralığı seçerek namaz vakitlerini sorgulayın</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="countrySelect">Ülke</label>
|
||||
<select id="countrySelect" class="form-control" onchange="loadStateDropdown(this.value, 'stateSelect'); document.getElementById('citySelect').innerHTML='<option value=\\'\\'>Önce il seçin...</option>';">
|
||||
<option value="">Yükleniyor...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="stateSelect">İl / Eyalet</label>
|
||||
<select id="stateSelect" class="form-control" onchange="loadCityDropdown(this.value, 'citySelect')">
|
||||
<option value="">Önce ülke seçin...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="citySelect">Şehir / İlçe</label>
|
||||
<select id="citySelect" class="form-control">
|
||||
<option value="">Önce il seçin...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:16px;">
|
||||
<div class="form-group">
|
||||
<label for="startDate">Başlangıç Tarihi</label>
|
||||
<input type="date" id="startDate" class="form-control" value="${today}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="endDate">Bitiş Tarihi</label>
|
||||
<input type="date" id="endDate" class="form-control" value="${today}">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">
|
||||
<button class="btn btn-primary" onclick="queryPrayerTimes()" id="btnQuery">🔍 Sorgula</button>
|
||||
<button class="btn btn-accent" onclick="queryToday()" id="btnToday">📅 Bugün</button>
|
||||
<div class="export-btns" style="margin-left:auto;">
|
||||
<button class="btn btn-outline btn-sm" onclick="exportCSV(currentData, 'namaz_vakitleri')">📄 CSV</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="exportJSON(currentData, 'namaz_vakitleri')">📋 JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tarih</th>
|
||||
<th>İmsak</th>
|
||||
<th>Güneş</th>
|
||||
<th>Öğle</th>
|
||||
<th>İkindi</th>
|
||||
<th>Akşam</th>
|
||||
<th>Yatsı</th>
|
||||
<th>Kıble</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="timesBody">
|
||||
<tr><td colspan="8" style="text-align:center; padding:40px; color:var(--text-light);">Bir şehir seçip sorgulayın</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
loadCountryDropdown('countrySelect');
|
||||
|
||||
async function queryPrayerTimes() {
|
||||
const cityId = document.getElementById('citySelect').value;
|
||||
if (!cityId) { showToast('Şehir seçin', 'warning'); return; }
|
||||
|
||||
const startDate = document.getElementById('startDate').value;
|
||||
const endDate = document.getElementById('endDate').value;
|
||||
const btn = document.getElementById('btnQuery');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span>';
|
||||
|
||||
try {
|
||||
const data = await apiFetch(`/prayer-times?cityId=${cityId}&startDate=${startDate}&endDate=${endDate}`);
|
||||
currentData = data;
|
||||
renderTable(data);
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🔍 Sorgula';
|
||||
}
|
||||
}
|
||||
|
||||
async function queryToday() {
|
||||
const cityId = document.getElementById('citySelect').value;
|
||||
if (!cityId) { showToast('Şehir seçin', 'warning'); return; }
|
||||
|
||||
try {
|
||||
const data = await apiFetch(`/prayer-times/today?cityId=${cityId}`);
|
||||
currentData = data ? [data] : [];
|
||||
renderTable(currentData);
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable(data) {
|
||||
const body = document.getElementById('timesBody');
|
||||
if (!data || data.length === 0) {
|
||||
body.innerHTML = '<tr><td colspan="8" style="text-align:center; padding:40px; color:var(--text-light);">Kayıt bulunamadı</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
const todayStr = today;
|
||||
body.innerHTML = data.map(row => {
|
||||
const rowDate = row.date ? row.date.split('T')[0] : '';
|
||||
const isToday = rowDate === todayStr;
|
||||
return `<tr style="${isToday ? 'background:rgba(26,107,78,0.06); font-weight:600;' : ''}">
|
||||
<td>${formatDate(row.date)}</td>
|
||||
<td>${row.fajr || '-'}</td>
|
||||
<td>${row.sunrise || '-'}</td>
|
||||
<td>${row.dhuhr || '-'}</td>
|
||||
<td>${row.asr || '-'}</td>
|
||||
<td>${row.maghrib || '-'}</td>
|
||||
<td>${row.isha || '-'}</td>
|
||||
<td>${row.qibla || '-'}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
155
frontend/src/pages/profile.html
Normal file
155
frontend/src/pages/profile.html
Normal file
@@ -0,0 +1,155 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Profil & API - hDiyanetProxy</title>
|
||||
<meta name="description" content="Profil bilgileri ve API token yönetimi">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout" id="app"></div>
|
||||
|
||||
<script src="/services/api.js"></script>
|
||||
<script src="/components/sidebar.js"></script>
|
||||
<script>
|
||||
if (!requireAuth()) throw new Error('Auth');
|
||||
|
||||
const user = getUser();
|
||||
const token = getToken();
|
||||
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = renderSidebar('profile') + `
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<h1>👤 Profil & API Token</h1>
|
||||
<p>Hesap bilgileriniz ve API erişim token'ınız</p>
|
||||
</div>
|
||||
|
||||
<!-- Profil Bilgileri -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📋 Hesap Bilgileri</h3>
|
||||
</div>
|
||||
<div style="display:grid; grid-template-columns:150px 1fr; gap:12px; font-size:14px;">
|
||||
<strong>Kullanıcı Adı:</strong> <span>${user ? user.username : '-'}</span>
|
||||
<strong>Rol:</strong> <span class="badge badge-${user?.role === 'admin' ? 'admin' : 'user'}">${user ? user.role : '-'}</span>
|
||||
<strong>ID:</strong> <span>${user ? user.id : '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Token -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>🔑 API Token</h3>
|
||||
<button class="btn btn-primary btn-sm" onclick="refreshToken()">🔄 Yeni Token Üret</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Mevcut Token (Bearer)</label>
|
||||
<textarea id="tokenDisplay" class="form-control" rows="3" readonly style="font-family:monospace; font-size:12px; resize:none;">${token || ''}</textarea>
|
||||
</div>
|
||||
<button class="btn btn-outline btn-sm" onclick="copyToken()">📋 Kopyala</button>
|
||||
</div>
|
||||
|
||||
<!-- API Kullanım -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📖 API Kullanımı</h3>
|
||||
</div>
|
||||
<div style="font-size:13px; color:var(--text-light); line-height:1.8;">
|
||||
<p>Tüm API isteklerinde <code>Authorization: Bearer {token}</code> header'ı gereklidir.</p>
|
||||
<br>
|
||||
<strong>Örnek istek:</strong>
|
||||
<div class="log-console" style="margin-top:8px; font-size:12px;">
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \\
|
||||
http://localhost:3000/api/v1/prayer-times/today?cityId=9541
|
||||
|
||||
# Tüm Endpointler:
|
||||
# POST /api/v1/auth/login
|
||||
# POST /api/v1/auth/register
|
||||
# GET /api/v1/places/countries
|
||||
# GET /api/v1/places/states?countryId={id}
|
||||
# GET /api/v1/places/cities?stateId={id}
|
||||
# GET /api/v1/places/city/{cityId}
|
||||
# GET /api/v1/prayer-times?cityId={id}&startDate={date}&endDate={date}
|
||||
# GET /api/v1/prayer-times/today?cityId={id}
|
||||
# GET /api/v1/ramadan?cityId={id}
|
||||
# GET /api/v1/ramadan/today?cityId={id}
|
||||
# GET /api/v1/eid?cityId={id}
|
||||
# POST /api/v1/admin/fetch/places
|
||||
# POST /api/v1/admin/fetch/yearly
|
||||
# POST /api/v1/admin/fetch/ramadan
|
||||
# POST /api/v1/admin/fetch/eid
|
||||
# POST /api/v1/admin/cache/clear
|
||||
# GET /api/v1/admin/logs
|
||||
# GET /api/v1/admin/users
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Şifre Değiştir -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>🔒 Şifre Değiştir</h3>
|
||||
</div>
|
||||
<form onsubmit="changePassword(event)">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="currentPassword">Mevcut Şifre</label>
|
||||
<input type="password" id="currentPassword" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newPassword">Yeni Şifre</label>
|
||||
<input type="password" id="newPassword" class="form-control" required minlength="6">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" id="btnChangePass">🔒 Şifreyi Güncelle</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
async function refreshToken() {
|
||||
try {
|
||||
const data = await apiFetch('/auth/refresh', { method: 'POST' });
|
||||
setToken(data.token);
|
||||
document.getElementById('tokenDisplay').value = data.token;
|
||||
showToast('Yeni token üretildi!', 'success');
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function copyToken() {
|
||||
const el = document.getElementById('tokenDisplay');
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
showToast('Token panoya kopyalandı', 'info');
|
||||
}
|
||||
|
||||
async function changePassword(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('btnChangePass');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
await apiFetch('/auth/change-password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
currentPassword: document.getElementById('currentPassword').value,
|
||||
newPassword: document.getElementById('newPassword').value
|
||||
})
|
||||
});
|
||||
showToast('Şifre başarıyla güncellendi!', 'success');
|
||||
document.getElementById('currentPassword').value = '';
|
||||
document.getElementById('newPassword').value = '';
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
141
frontend/src/pages/ramadan.html
Normal file
141
frontend/src/pages/ramadan.html
Normal file
@@ -0,0 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ramazan / İftar - hDiyanetProxy</title>
|
||||
<meta name="description" content="Ramazan imsakiyesi ve iftar vakitleri">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout" id="app"></div>
|
||||
|
||||
<script src="/services/api.js"></script>
|
||||
<script src="/components/sidebar.js"></script>
|
||||
<script>
|
||||
if (!requireAuth()) throw new Error('Auth');
|
||||
|
||||
let currentData = [];
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = renderSidebar('ramadan') + `
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<h1>🌙 Ramazan / İftar Vakitleri</h1>
|
||||
<p>Ramazan imsakiyesi ve iftar vakitlerini sorgulayın</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="countrySelect">Ülke</label>
|
||||
<select id="countrySelect" class="form-control" onchange="loadStateDropdown(this.value, 'stateSelect'); document.getElementById('citySelect').innerHTML='<option value=\\'\\'>Önce il seçin...</option>';">
|
||||
<option value="">Yükleniyor...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="stateSelect">İl / Eyalet</label>
|
||||
<select id="stateSelect" class="form-control" onchange="loadCityDropdown(this.value, 'citySelect')">
|
||||
<option value="">Önce ülke seçin...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="citySelect">Şehir / İlçe</label>
|
||||
<select id="citySelect" class="form-control">
|
||||
<option value="">Önce il seçin...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">
|
||||
<button class="btn btn-accent" onclick="queryRamadan()" id="btnQuery">🌙 Ramazan Takvimini Getir</button>
|
||||
<button class="btn btn-primary" onclick="queryRamadanToday()" id="btnToday">📅 Bugün</button>
|
||||
<div class="export-btns" style="margin-left:auto;">
|
||||
<button class="btn btn-outline btn-sm" onclick="exportCSV(currentData, 'ramazan_vakitleri')">📄 CSV</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="exportJSON(currentData, 'ramazan_vakitleri')">📋 JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tarih</th>
|
||||
<th>İmsak (Sahur)</th>
|
||||
<th>Güneş</th>
|
||||
<th>Öğle</th>
|
||||
<th>İkindi</th>
|
||||
<th>Akşam (İftar)</th>
|
||||
<th>Yatsı</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="timesBody">
|
||||
<tr><td colspan="7" style="text-align:center; padding:40px; color:var(--text-light);">Bir şehir seçip sorgulayın</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
loadCountryDropdown('countrySelect');
|
||||
|
||||
async function queryRamadan() {
|
||||
const cityId = document.getElementById('citySelect').value;
|
||||
if (!cityId) { showToast('Şehir seçin', 'warning'); return; }
|
||||
|
||||
const btn = document.getElementById('btnQuery');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span>';
|
||||
|
||||
try {
|
||||
const data = await apiFetch(`/ramadan?cityId=${cityId}`);
|
||||
currentData = data;
|
||||
renderRamadanTable(data);
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🌙 Ramazan Takvimini Getir';
|
||||
}
|
||||
}
|
||||
|
||||
async function queryRamadanToday() {
|
||||
const cityId = document.getElementById('citySelect').value;
|
||||
if (!cityId) { showToast('Şehir seçin', 'warning'); return; }
|
||||
|
||||
try {
|
||||
const data = await apiFetch(`/ramadan/today?cityId=${cityId}`);
|
||||
currentData = data ? [data] : [];
|
||||
renderRamadanTable(currentData);
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderRamadanTable(data) {
|
||||
const body = document.getElementById('timesBody');
|
||||
if (!data || data.length === 0) {
|
||||
body.innerHTML = '<tr><td colspan="7" style="text-align:center; padding:40px; color:var(--text-light);">Kayıt bulunamadı</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
body.innerHTML = data.map(row => {
|
||||
const rowDate = row.date ? row.date.split('T')[0] : '';
|
||||
const isToday = rowDate === todayStr;
|
||||
return `<tr class="${isToday ? 'iftar-row' : ''}" style="${isToday ? 'background:rgba(232,168,56,0.1);' : ''}">
|
||||
<td>${formatDate(row.date)} ${isToday ? '<span class="badge badge-warning">BUGÜN</span>' : ''}</td>
|
||||
<td>${row.fajr || '-'}</td>
|
||||
<td>${row.sunrise || '-'}</td>
|
||||
<td>${row.dhuhr || '-'}</td>
|
||||
<td>${row.asr || '-'}</td>
|
||||
<td style="${isToday ? 'color:var(--accent-dark); font-weight:700; font-size:15px;' : ''}">${row.maghrib || '-'} ${isToday ? '🌙' : ''}</td>
|
||||
<td>${row.isha || '-'}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
127
frontend/src/services/api.js
Normal file
127
frontend/src/services/api.js
Normal file
@@ -0,0 +1,127 @@
|
||||
// hDiyanetProxy - Frontend API Servisi
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
// Token yönetimi
|
||||
function getToken() {
|
||||
return localStorage.getItem('hdiyanet_token');
|
||||
}
|
||||
|
||||
function setToken(token) {
|
||||
localStorage.setItem('hdiyanet_token', token);
|
||||
}
|
||||
|
||||
function getUser() {
|
||||
const user = localStorage.getItem('hdiyanet_user');
|
||||
return user ? JSON.parse(user) : null;
|
||||
}
|
||||
|
||||
function setUser(user) {
|
||||
localStorage.setItem('hdiyanet_user', JSON.stringify(user));
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('hdiyanet_token');
|
||||
localStorage.removeItem('hdiyanet_user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
// Auth kontrolü - token yoksa login'e yönlendir
|
||||
function requireAuth() {
|
||||
if (!getToken()) {
|
||||
window.location.href = '/login';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API çağrısı yardımcı fonksiyonu
|
||||
async function apiFetch(endpoint, options = {}) {
|
||||
const token = getToken();
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
...options.headers
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
...options,
|
||||
headers
|
||||
});
|
||||
|
||||
// 401 ise login'e yönlendir
|
||||
if (response.status === 401) {
|
||||
logout();
|
||||
throw new Error('Oturum süresi doldu');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || data.errors?.[0]?.msg || 'Bir hata oluştu');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Toast bildirimi göster
|
||||
function showToast(message, type = 'info') {
|
||||
let container = document.querySelector('.toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Tarih formatlama
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString('tr-TR');
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleString('tr-TR');
|
||||
}
|
||||
|
||||
// CSV dışa aktarma
|
||||
function exportCSV(data, filename) {
|
||||
if (!data || data.length === 0) return;
|
||||
const headers = Object.keys(data[0]);
|
||||
const csv = [
|
||||
headers.join(','),
|
||||
...data.map(row => headers.map(h => `"${row[h] || ''}"`).join(','))
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${filename}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// JSON dışa aktarma
|
||||
function exportJSON(data, filename) {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${filename}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
567
frontend/src/style.css
Normal file
567
frontend/src/style.css
Normal file
@@ -0,0 +1,567 @@
|
||||
/* hDiyanetProxy - Ana Stil Dosyası */
|
||||
:root {
|
||||
--primary: #1a6b4e;
|
||||
--primary-light: #2d9d6f;
|
||||
--primary-dark: #0f4a35;
|
||||
--accent: #e8a838;
|
||||
--accent-dark: #c48a1f;
|
||||
--bg: #f5f7fa;
|
||||
--bg-card: #ffffff;
|
||||
--text: #1e293b;
|
||||
--text-light: #64748b;
|
||||
--text-lighter: #94a3b8;
|
||||
--border: #e2e8f0;
|
||||
--success: #16a34a;
|
||||
--error: #dc2626;
|
||||
--warning: #f59e0b;
|
||||
--info: #3b82f6;
|
||||
--shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.08), 0 2px 4px -1px rgba(0,0,0,0.04);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.08), 0 4px 6px -2px rgba(0,0,0,0.04);
|
||||
--radius: 8px;
|
||||
--radius-lg: 12px;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a { color: var(--primary); text-decoration: none; }
|
||||
a:hover { color: var(--primary-light); }
|
||||
|
||||
/* --- Layout --- */
|
||||
.app-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: var(--primary-dark);
|
||||
color: #fff;
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 20px 20px 16px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sidebar-header small {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: rgba(255,255,255,0.5);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 12px 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 20px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover {
|
||||
color: #fff;
|
||||
background: rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.sidebar-nav a.active {
|
||||
color: #fff;
|
||||
background: rgba(255,255,255,0.12);
|
||||
border-left-color: var(--accent);
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-section {
|
||||
padding: 16px 20px 6px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: rgba(255,255,255,0.35);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sidebar-footer .user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.sidebar-footer .user-badge {
|
||||
background: var(--accent);
|
||||
color: var(--primary-dark);
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
margin-left: 260px;
|
||||
padding: 24px 32px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--text-light);
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* --- Cards --- */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* --- Stats Grid --- */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card .stat-icon {
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-card .stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.stat-card .stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-light);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* --- Buttons --- */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover { background: var(--primary-light); }
|
||||
|
||||
.btn-accent {
|
||||
background: var(--accent);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
.btn-accent:hover { background: var(--accent-dark); color: #fff; }
|
||||
|
||||
.btn-danger {
|
||||
background: var(--error);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-danger:hover { background: #b91c1c; }
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
.btn-outline:hover { border-color: var(--primary); color: var(--primary); }
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* --- Forms --- */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
color: var(--text);
|
||||
background: var(--bg-card);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(26,107,78,0.1);
|
||||
}
|
||||
|
||||
select.form-control {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* --- Tables --- */
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
thead th {
|
||||
background: var(--bg);
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--text-light);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 2px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tbody td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: rgba(26,107,78,0.03);
|
||||
}
|
||||
|
||||
/* --- Badges --- */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.badge-success { background: #dcfce7; color: #166534; }
|
||||
.badge-error { background: #fef2f2; color: #991b1b; }
|
||||
.badge-info { background: #dbeafe; color: #1e40af; }
|
||||
.badge-warning { background: #fef9c3; color: #854d0e; }
|
||||
.badge-admin { background: var(--accent); color: var(--primary-dark); }
|
||||
.badge-user { background: #e2e8f0; color: #475569; }
|
||||
|
||||
/* --- Toast / Alert --- */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
padding: 12px 20px;
|
||||
border-radius: var(--radius);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
box-shadow: var(--shadow-lg);
|
||||
animation: slideIn 0.3s ease;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.toast-success { background: var(--success); }
|
||||
.toast-error { background: var(--error); }
|
||||
.toast-info { background: var(--info); }
|
||||
.toast-warning { background: var(--warning); }
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* --- Login Page --- */
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 50%, var(--primary-light) 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.login-card h1 {
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.login-card .subtitle {
|
||||
text-align: center;
|
||||
color: var(--text-light);
|
||||
font-size: 13px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.login-card .btn-primary {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 15px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
background: #fef2f2;
|
||||
color: var(--error);
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* --- Log Console --- */
|
||||
.log-console {
|
||||
background: #1e293b;
|
||||
color: #a5f3fc;
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.log-console .log-success { color: #4ade80; }
|
||||
.log-console .log-error { color: #f87171; }
|
||||
.log-console .log-info { color: #93c5fd; }
|
||||
|
||||
/* --- Loading --- */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(255,255,255,0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* --- Modal --- */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
width: 90%;
|
||||
max-width: 480px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.modal h3 {
|
||||
margin-bottom: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* --- Responsive --- */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
.mobile-toggle {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-toggle {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 200;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
padding: 8px 12px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Iftar vurgulu satır */
|
||||
tr.iftar-row {
|
||||
background: rgba(232,168,56,0.08) !important;
|
||||
}
|
||||
|
||||
tr.iftar-row td {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Export butonları */
|
||||
.export-btns {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
Reference in New Issue
Block a user