@artilingo/artiframe-cli 2.0.3 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +660 -660
- package/bin/artiframe.js +0 -0
- package/bin/artiframe.php +0 -0
- package/core-stubs/app/ApiAuthControl.php +76 -0
- package/core-stubs/app/ApiControl.php +81 -35
- package/core-stubs/app/AuthControl.php +39 -0
- package/core-stubs/app/Database.php +36 -35
- package/core-stubs/app/DotEnv.php +19 -1
- package/core-stubs/app/ViewControl.php +47 -1
- package/core-stubs/bin/SQLMethod.php +261 -0
- package/core-stubs/bin/SystemMethod.php +115 -74
- package/core-stubs/bin/ViewMethod.php +19 -5
- package/core-stubs/docs/de.html +4325 -4347
- package/core-stubs/docs/en.html +4325 -4347
- package/core-stubs/docs/es.html +4325 -4347
- package/core-stubs/docs/fr.html +4325 -4347
- package/core-stubs/docs/tr.html +117 -46
- package/core-stubs/public/403.php +52 -0
- package/core-stubs/public/assets/css/components/header.css +174 -0
- package/core-stubs/public/assets/css/components/mobilenav.css +198 -0
- package/core-stubs/public/assets/css/components/sidebar.css +184 -0
- package/core-stubs/public/assets/css/root/app.css +975 -38
- package/core-stubs/public/assets/css/root/root.css +64 -0
- package/core-stubs/public/assets/js/components/header.js +380 -0
- package/core-stubs/public/assets/js/components/mobilenav.js +97 -0
- package/core-stubs/public/assets/js/components/theme-modal.js +74 -0
- package/core-stubs/public/assets/js/root/app.js +1870 -0
- package/core-stubs/public/includes/footer.php +4 -5
- package/core-stubs/public/includes/head-auth.php +65 -0
- package/core-stubs/public/includes/head.php +56 -17
- package/core-stubs/public/includes/header.php +135 -1
- package/core-stubs/public/includes/mobilenav.php +75 -3
- package/core-stubs/public/includes/sidebar.php +186 -1
- package/core-stubs/public/includes/theme-modal.php +1 -0
- package/db-studio/debug.log +3 -0
- package/db-studio/index.html +1755 -0
- package/db-studio/launcher.html +102 -0
- package/db-studio/launcher.js +180 -0
- package/db-studio/logo.svg +22 -0
- package/db-studio/main.js +568 -0
- package/db-studio/package-lock.json +1034 -0
- package/db-studio/package.json +16 -0
- package/db-studio/preload.js +29 -0
- package/devops-studio/api-inspector.js +222 -0
- package/devops-studio/index.html +1892 -0
- package/devops-studio/logo.svg +22 -0
- package/devops-studio/main.js +67 -0
- package/devops-studio/make-wizard.js +635 -0
- package/devops-studio/package-lock.json +871 -0
- package/devops-studio/package.json +12 -0
- package/package.json +5 -2
- package/scripts/postinstall.js +44 -8
- package/server-studio/.json-404-guard +1 -0
- package/server-studio/index.html +283 -0
- package/server-studio/logo.svg +22 -0
- package/server-studio/main.js +133 -0
- package/server-studio/package-lock.json +1179 -0
- package/server-studio/package.json +15 -0
- package/server-studio/router.php +99 -0
- package/src/App.php +29 -0
- package/src/Commands/AddCommand.php +26 -9
- package/src/Commands/DbExportCommand.php +131 -0
- package/src/Commands/DbStartCommand.php +46 -0
- package/src/Commands/DevopsCommand.php +35 -0
- package/src/Commands/GoCommand.php +48 -48
- package/src/Commands/ListCommand.php +68 -68
- package/src/Commands/MakeApiCommand.php +6 -1
- package/src/Commands/MakeViewCommand.php +25 -2
- package/src/Commands/NewProjectCommand.php +16 -11
- package/src/Commands/OptimizeCommand.php +61 -0
- package/src/Commands/RemoveCommand.php +36 -7
- package/src/Commands/ServeCommand.php +46 -91
- package/src/Commands/ShowCommand.php +24 -24
- package/src/Commands/UpgradeCommand.php +39 -10
- package/stubs/make/api-switch-case.stub +1 -1
- package/stubs/make/auth-api-standart.stub +20 -0
- package/stubs/make/auth-api-switch-case.stub +38 -0
- package/stubs/make/auth-view.stub +26 -0
- package/stubs/make/class.stub +10 -2
- package/stubs/make/view.stub +3 -0
- package/stubs/service/image.stub +334 -334
- package/stubs/service/iyzico.stub +637 -637
- package/stubs/service/jwt.stub +312 -312
- package/stubs/service/phpmailer.stub +143 -143
- package/stubs/service/pusher.stub +232 -232
- package/stubs/service/qrcode.stub +169 -169
- package/stubs/service/redis.stub +361 -361
- package/stubs/service/s3.stub +377 -377
- package/stubs/service/stripe.stub +526 -526
- package/stubs/service/twilio.stub +361 -361
|
@@ -1,9 +1,55 @@
|
|
|
1
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* ArtiFrame Global JS
|
|
3
5
|
* Kural: Event'ler (Click, Submit vb.) asla id veya class üzerinden bağlanmaz!
|
|
4
6
|
* Tüm etkileşimli elemanlar `data-js` özniteliği üzerinden yönetilir.
|
|
5
7
|
*/
|
|
6
8
|
|
|
9
|
+
/* ---- Global Yardımcı Fonksiyonlar ---- */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* HTML özel karakterlerini escape et (XSS koruması).
|
|
13
|
+
*/
|
|
14
|
+
window.escHtml = function(str) {
|
|
15
|
+
if (!str) return '';
|
|
16
|
+
return String(str)
|
|
17
|
+
.replace(/&/g, '&')
|
|
18
|
+
.replace(/</g, '<')
|
|
19
|
+
.replace(/>/g, '>')
|
|
20
|
+
.replace(/"/g, '"');
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Toast bildirimi göster.
|
|
25
|
+
*/
|
|
26
|
+
window.showToast = function(msg, type = 'success') {
|
|
27
|
+
const tc = document.querySelector('.toast-container');
|
|
28
|
+
if (!tc) return;
|
|
29
|
+
const t = document.createElement('div');
|
|
30
|
+
t.className = `toast ${type}`;
|
|
31
|
+
t.innerHTML = `<span class="toast-message">${window.escHtml(msg)}</span>`;
|
|
32
|
+
tc.appendChild(t);
|
|
33
|
+
requestAnimationFrame(() => t.classList.add('show'));
|
|
34
|
+
setTimeout(() => { t.classList.remove('show'); setTimeout(() => t.remove(), 500); }, 3500);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* CSRF korumalı FormData POST isteği atar.
|
|
39
|
+
* CSRF token'i window.SQ_CONFIG veya window.SD_CONFIG üzerinden alır.
|
|
40
|
+
*/
|
|
41
|
+
window.apiPost = async function(url, body) {
|
|
42
|
+
const cfg = window.SQ_CONFIG || window.SD_CONFIG || {};
|
|
43
|
+
const metaCsrf = document.querySelector('meta[name="csrf-token"]')?.content;
|
|
44
|
+
const csrfToken = cfg.csrfToken || metaCsrf || '';
|
|
45
|
+
|
|
46
|
+
const fd = new FormData();
|
|
47
|
+
fd.append('csrf_token', csrfToken);
|
|
48
|
+
for (const [k, v] of Object.entries(body)) fd.append(k, v);
|
|
49
|
+
const res = await fetch(url, { method: 'POST', body: fd });
|
|
50
|
+
return res.json();
|
|
51
|
+
};
|
|
52
|
+
|
|
7
53
|
document.addEventListener('DOMContentLoaded', () => {
|
|
8
54
|
// 1. Tema Değiştirme (Theme Toggle) Mantığı
|
|
9
55
|
// Sayfanın herhangi bir yerinde <button data-js="theme-toggle"> tıklandığında çalışır.
|
|
@@ -28,3 +74,1827 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
28
74
|
document.documentElement.setAttribute('data-mode', savedMode);
|
|
29
75
|
}
|
|
30
76
|
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Global Toaster (Bildirim) Mekanizması
|
|
80
|
+
* Kullanımı: window.showToast('İşlem başarılı!', 'success');
|
|
81
|
+
* type: 'success', 'error', 'warning', 'info'
|
|
82
|
+
*/
|
|
83
|
+
window.showToast = function(message, type = 'success', duration = 3500) {
|
|
84
|
+
// 1. Container kontrolü (Yoksa oluştur)
|
|
85
|
+
let container = document.querySelector('.toast-container');
|
|
86
|
+
if (!container) {
|
|
87
|
+
container = document.createElement('div');
|
|
88
|
+
container.className = 'toast-container';
|
|
89
|
+
document.body.appendChild(container);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 2. Toast elementi oluştur
|
|
93
|
+
const toast = document.createElement('div');
|
|
94
|
+
toast.className = `toast ${type}`;
|
|
95
|
+
|
|
96
|
+
// Mesaj içeriği
|
|
97
|
+
const textNode = document.createElement('div');
|
|
98
|
+
textNode.className = 'toast-message';
|
|
99
|
+
textNode.textContent = message;
|
|
100
|
+
|
|
101
|
+
toast.appendChild(textNode);
|
|
102
|
+
container.appendChild(toast);
|
|
103
|
+
|
|
104
|
+
// 3. Animasyonla ekranda göster
|
|
105
|
+
requestAnimationFrame(() => {
|
|
106
|
+
toast.classList.add('show');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// 4. Süre bitiminde kaldır
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
toast.classList.remove('show');
|
|
112
|
+
setTimeout(() => toast.remove(), 500); // Transition süresi kadar bekle
|
|
113
|
+
}, duration);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Global Image Viewer (Viewer.js Native Bind)
|
|
118
|
+
* Soru duvarı, Soru detay ve Profil gibi tüm galeri kapsayıcılarına otomatik uygulanır.
|
|
119
|
+
*/
|
|
120
|
+
window.initImageViewer = function(container = document) {
|
|
121
|
+
if (typeof Viewer !== "undefined") {
|
|
122
|
+
container.querySelectorAll('.post-gallery, .sq-gorsel-grid, .sd-gorsel-grid').forEach(gallery => {
|
|
123
|
+
if (gallery.dataset.viewerInit) return;
|
|
124
|
+
gallery.dataset.viewerInit = '1';
|
|
125
|
+
|
|
126
|
+
new Viewer(gallery, {
|
|
127
|
+
toolbar: false, // Alttaki zoom, rotate butonlarını gizle
|
|
128
|
+
navbar: false, // Alttaki küçük resimleri (thumbnails) gizle
|
|
129
|
+
title: false, // Resim ismini gizle
|
|
130
|
+
tooltip: false, // Zoom yüzdesini gizle
|
|
131
|
+
movable: true, // Sürüklemeye izin ver
|
|
132
|
+
zoomable: true, // Zoom'a izin ver
|
|
133
|
+
rotatable: false,
|
|
134
|
+
scalable: false,
|
|
135
|
+
transition: true, // CSS animasyonları ile pürüzsüz geçiş
|
|
136
|
+
fullscreen: true,
|
|
137
|
+
keyboard: true,
|
|
138
|
+
|
|
139
|
+
// Güvenli Özel Oklar (Custom Desktop Arrows)
|
|
140
|
+
ready() {
|
|
141
|
+
const viewerInstance = this.viewer;
|
|
142
|
+
// Sadece 1'den fazla görsel varsa okları ekle
|
|
143
|
+
if (viewerInstance.length > 1) {
|
|
144
|
+
const vContainer = viewerInstance.viewer; // Kütüphanenin oluşturduğu ana kapsayıcı
|
|
145
|
+
|
|
146
|
+
// Geri Oku
|
|
147
|
+
const prevBtn = document.createElement('div');
|
|
148
|
+
prevBtn.className = 'custom-viewer-arrow prev';
|
|
149
|
+
prevBtn.innerHTML = '<i class="fa-solid fa-chevron-left"></i>';
|
|
150
|
+
prevBtn.onclick = (e) => { e.stopPropagation(); viewerInstance.prev(true); };
|
|
151
|
+
|
|
152
|
+
// İleri Oku
|
|
153
|
+
const nextBtn = document.createElement('div');
|
|
154
|
+
nextBtn.className = 'custom-viewer-arrow next';
|
|
155
|
+
nextBtn.innerHTML = '<i class="fa-solid fa-chevron-right"></i>';
|
|
156
|
+
nextBtn.onclick = (e) => { e.stopPropagation(); viewerInstance.next(true); };
|
|
157
|
+
|
|
158
|
+
// Kapsayıcıya enjekte et
|
|
159
|
+
vContainer.appendChild(prevBtn);
|
|
160
|
+
vContainer.appendChild(nextBtn);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
169
|
+
window.initImageViewer();
|
|
170
|
+
|
|
171
|
+
// Raporlama Modalı Olayları
|
|
172
|
+
const reportModal = document.getElementById('global-report-modal');
|
|
173
|
+
const closeBtn = document.getElementById('close-global-report-modal');
|
|
174
|
+
const reportForm = document.getElementById('global-report-form');
|
|
175
|
+
const aciklamaInput = document.getElementById('global-report-aciklama');
|
|
176
|
+
const aciklamaCount = document.getElementById('global-report-aciklama-count');
|
|
177
|
+
|
|
178
|
+
if (reportModal) {
|
|
179
|
+
closeBtn.addEventListener('click', () => {
|
|
180
|
+
reportModal.classList.remove('show');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
aciklamaInput.addEventListener('input', function() {
|
|
184
|
+
aciklamaCount.textContent = this.value.length;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
reportForm.addEventListener('submit', function(e) {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
const btn = document.getElementById('submit-global-report-btn');
|
|
190
|
+
const originalText = btn.innerHTML;
|
|
191
|
+
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Gönderiliyor...';
|
|
192
|
+
btn.disabled = true;
|
|
193
|
+
|
|
194
|
+
const formData = new FormData(this);
|
|
195
|
+
formData.append('action', 'icerik_raporla');
|
|
196
|
+
|
|
197
|
+
const csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
|
198
|
+
if (csrfMeta && !formData.has('csrf_token')) formData.append('csrf_token', csrfMeta.content);
|
|
199
|
+
|
|
200
|
+
fetch('/api/switch-case/akis', {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
body: formData
|
|
203
|
+
})
|
|
204
|
+
.then(r => r.json())
|
|
205
|
+
.then(data => {
|
|
206
|
+
if (data.status === 'success') {
|
|
207
|
+
reportModal.classList.remove('show');
|
|
208
|
+
if (window.showToast) window.showToast(data.message, 'success');
|
|
209
|
+
} else {
|
|
210
|
+
if (window.showToast) window.showToast(data.message || 'Bir hata oluştu.', 'error');
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
.catch(err => {
|
|
214
|
+
console.error("Rapor Hatası:", err);
|
|
215
|
+
if (window.showToast) window.showToast('Sistemsel bir hata oluştu.', 'error');
|
|
216
|
+
})
|
|
217
|
+
.finally(() => {
|
|
218
|
+
btn.innerHTML = originalText;
|
|
219
|
+
btn.disabled = false;
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Global İçerik Raporlama
|
|
227
|
+
* 1: Soru, 2: Gönderi, 3: Soru Yorumu, 4: Gönderi Yorumu, 5: Çözüm
|
|
228
|
+
*/
|
|
229
|
+
window.reportContent = function(icerikTuru, icerikId) {
|
|
230
|
+
const modal = document.getElementById('global-report-modal');
|
|
231
|
+
if (!modal) {
|
|
232
|
+
console.error("Raporlama modalı bulunamadı.");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Formu sıfırla
|
|
237
|
+
document.getElementById('global-report-form').reset();
|
|
238
|
+
document.getElementById('global-report-aciklama-count').textContent = '0';
|
|
239
|
+
|
|
240
|
+
// ID'leri set et
|
|
241
|
+
document.getElementById('global-report-icerikTuru').value = icerikTuru;
|
|
242
|
+
document.getElementById('global-report-icerikId').value = icerikId;
|
|
243
|
+
|
|
244
|
+
// Modalı aç
|
|
245
|
+
requestAnimationFrame(() => {
|
|
246
|
+
modal.classList.add('show');
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Global Confirm (Onay) Mekanizması
|
|
252
|
+
* Kullanımı: window.showConfirm('Emin misiniz?', 'Açıklama', { confirmText: 'Sil', confirmClass: 'btn-danger', cancelText: 'Vazgeç' }).then(res => ...)
|
|
253
|
+
*/
|
|
254
|
+
window.showConfirm = function(title, message, options = {}) {
|
|
255
|
+
return new Promise((resolve) => {
|
|
256
|
+
const overlay = document.createElement('div');
|
|
257
|
+
overlay.className = 'confirm-overlay';
|
|
258
|
+
|
|
259
|
+
const modal = document.createElement('div');
|
|
260
|
+
modal.className = 'confirm-modal';
|
|
261
|
+
|
|
262
|
+
const titleEl = document.createElement('h3');
|
|
263
|
+
titleEl.className = 'confirm-title';
|
|
264
|
+
titleEl.textContent = title;
|
|
265
|
+
|
|
266
|
+
const messageEl = document.createElement('p');
|
|
267
|
+
messageEl.className = 'confirm-message';
|
|
268
|
+
messageEl.textContent = message;
|
|
269
|
+
|
|
270
|
+
const actionsEl = document.createElement('div');
|
|
271
|
+
actionsEl.className = 'confirm-actions';
|
|
272
|
+
|
|
273
|
+
const cancelBtn = document.createElement('button');
|
|
274
|
+
cancelBtn.className = `btn ${options.cancelClass || 'btn-outline'}`;
|
|
275
|
+
if (options.cancelStyle) cancelBtn.style.cssText = options.cancelStyle;
|
|
276
|
+
cancelBtn.textContent = options.cancelText || 'İptal';
|
|
277
|
+
|
|
278
|
+
const confirmBtn = document.createElement('button');
|
|
279
|
+
confirmBtn.className = `btn ${options.confirmClass || 'btn-primary'}`;
|
|
280
|
+
if (options.confirmStyle) confirmBtn.style.cssText = options.confirmStyle;
|
|
281
|
+
confirmBtn.textContent = options.confirmText || 'Tamam';
|
|
282
|
+
|
|
283
|
+
const close = (result) => {
|
|
284
|
+
overlay.classList.remove('show');
|
|
285
|
+
setTimeout(() => overlay.remove(), 300);
|
|
286
|
+
resolve(result);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
cancelBtn.addEventListener('click', () => close(false));
|
|
290
|
+
confirmBtn.addEventListener('click', () => close(true));
|
|
291
|
+
|
|
292
|
+
actionsEl.appendChild(cancelBtn);
|
|
293
|
+
actionsEl.appendChild(confirmBtn);
|
|
294
|
+
|
|
295
|
+
modal.appendChild(titleEl);
|
|
296
|
+
modal.appendChild(messageEl);
|
|
297
|
+
modal.appendChild(actionsEl);
|
|
298
|
+
|
|
299
|
+
overlay.appendChild(modal);
|
|
300
|
+
document.body.appendChild(overlay);
|
|
301
|
+
|
|
302
|
+
requestAnimationFrame(() => {
|
|
303
|
+
requestAnimationFrame(() => overlay.classList.add('show'));
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Global Password Prompt Mekanizması
|
|
310
|
+
* Hassas işlemlerde şifre doğrulamak için kullanılır.
|
|
311
|
+
*/
|
|
312
|
+
window.showPasswordPrompt = function(title, message, options = {}) {
|
|
313
|
+
return new Promise((resolve) => {
|
|
314
|
+
const overlay = document.createElement('div');
|
|
315
|
+
overlay.className = 'confirm-overlay';
|
|
316
|
+
|
|
317
|
+
const modal = document.createElement('div');
|
|
318
|
+
modal.className = 'confirm-modal';
|
|
319
|
+
|
|
320
|
+
const titleEl = document.createElement('h3');
|
|
321
|
+
titleEl.className = 'confirm-title';
|
|
322
|
+
titleEl.textContent = title;
|
|
323
|
+
|
|
324
|
+
const messageEl = document.createElement('p');
|
|
325
|
+
messageEl.className = 'confirm-message';
|
|
326
|
+
messageEl.textContent = message;
|
|
327
|
+
|
|
328
|
+
const inputEl = document.createElement('input');
|
|
329
|
+
inputEl.type = 'password';
|
|
330
|
+
inputEl.className = 'confirm-prompt-input';
|
|
331
|
+
inputEl.placeholder = 'Lütfen şifrenizi girin...';
|
|
332
|
+
|
|
333
|
+
const actionsEl = document.createElement('div');
|
|
334
|
+
actionsEl.className = 'confirm-actions';
|
|
335
|
+
|
|
336
|
+
const cancelBtn = document.createElement('button');
|
|
337
|
+
cancelBtn.className = `btn ${options.cancelClass || 'btn-outline'}`;
|
|
338
|
+
cancelBtn.textContent = options.cancelText || 'İptal';
|
|
339
|
+
|
|
340
|
+
const confirmBtn = document.createElement('button');
|
|
341
|
+
confirmBtn.className = `btn ${options.confirmClass || 'btn-primary'}`;
|
|
342
|
+
confirmBtn.textContent = options.confirmText || 'Doğrula';
|
|
343
|
+
|
|
344
|
+
const close = (result) => {
|
|
345
|
+
overlay.classList.remove('show');
|
|
346
|
+
setTimeout(() => overlay.remove(), 300);
|
|
347
|
+
resolve(result);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
cancelBtn.addEventListener('click', () => close(null));
|
|
351
|
+
confirmBtn.addEventListener('click', () => {
|
|
352
|
+
if (!inputEl.value.trim()) {
|
|
353
|
+
window.showToast('Uyarı', 'Lütfen şifrenizi girin!', 'warning');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
close(inputEl.value);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Enter tuşu desteği
|
|
360
|
+
inputEl.addEventListener('keyup', (e) => {
|
|
361
|
+
if (e.key === 'Enter') confirmBtn.click();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
actionsEl.appendChild(cancelBtn);
|
|
365
|
+
actionsEl.appendChild(confirmBtn);
|
|
366
|
+
|
|
367
|
+
modal.appendChild(titleEl);
|
|
368
|
+
modal.appendChild(messageEl);
|
|
369
|
+
modal.appendChild(inputEl);
|
|
370
|
+
modal.appendChild(actionsEl);
|
|
371
|
+
|
|
372
|
+
overlay.appendChild(modal);
|
|
373
|
+
document.body.appendChild(overlay);
|
|
374
|
+
|
|
375
|
+
requestAnimationFrame(() => {
|
|
376
|
+
requestAnimationFrame(() => {
|
|
377
|
+
overlay.classList.add('show');
|
|
378
|
+
inputEl.focus();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
/* --- Global Comment Manager --- */
|
|
385
|
+
window.CommentManager = {
|
|
386
|
+
modal: null,
|
|
387
|
+
aktifSoruId: null,
|
|
388
|
+
aktifUstYorumId: null,
|
|
389
|
+
yorumlarYuklendi: false,
|
|
390
|
+
|
|
391
|
+
init() {
|
|
392
|
+
if (this.modal) return;
|
|
393
|
+
const html = `
|
|
394
|
+
<div class="cm-overlay" id="cmOverlay">
|
|
395
|
+
<div class="cm-modal">
|
|
396
|
+
<div class="cm-header">
|
|
397
|
+
<h2>Yorumlar</h2>
|
|
398
|
+
<button class="cm-close" id="cmClose"><i class="fa-solid fa-xmark"></i></button>
|
|
399
|
+
</div>
|
|
400
|
+
<div class="cm-body" id="cmBody">
|
|
401
|
+
<div class="sq-yorumlar-loading"><i class="fa-solid fa-spinner fa-spin"></i> Yükleniyor...</div>
|
|
402
|
+
</div>
|
|
403
|
+
<div class="cm-footer">
|
|
404
|
+
<div class="sq-yorum-input-wrap">
|
|
405
|
+
<div class="sq-reply-status" id="cmReplyStatus">
|
|
406
|
+
<span>Yanıtlanıyor: <strong id="cmReplyToUsername"></strong></span>
|
|
407
|
+
<button type="button" class="sq-cancel-reply" id="cmBtnCancelReply"><i class="fa-solid fa-xmark"></i> İptal</button>
|
|
408
|
+
</div>
|
|
409
|
+
<!-- Mention Autocomplete Dropdown -->
|
|
410
|
+
<div class="cm-mention-dropdown" id="cmMentionDropdown"></div>
|
|
411
|
+
<div class="sq-yorum-input-row">
|
|
412
|
+
<div class="sq-yorum-input cm-editor"
|
|
413
|
+
id="cmYorumInput"
|
|
414
|
+
contenteditable="true"
|
|
415
|
+
data-placeholder="Yorum yaz..."
|
|
416
|
+
role="textbox"
|
|
417
|
+
aria-multiline="true"
|
|
418
|
+
spellcheck="true"></div>
|
|
419
|
+
<button class="btn btn-primary btn-sm" id="cmBtnSendYorum">
|
|
420
|
+
<i class="fa-solid fa-paper-plane"></i>
|
|
421
|
+
</button>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
`;
|
|
428
|
+
document.body.insertAdjacentHTML('beforeend', html);
|
|
429
|
+
this.modal = document.getElementById('cmOverlay');
|
|
430
|
+
|
|
431
|
+
document.getElementById('cmClose').addEventListener('click', () => this.close());
|
|
432
|
+
this.modal.addEventListener('click', (e) => {
|
|
433
|
+
if (e.target === this.modal) this.close();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
document.getElementById('cmBtnCancelReply').addEventListener('click', () => {
|
|
437
|
+
this.aktifUstYorumId = null;
|
|
438
|
+
document.getElementById('cmReplyStatus').classList.remove('active');
|
|
439
|
+
const editor = document.getElementById('cmYorumInput');
|
|
440
|
+
// @mention tagini sil, sadece tag ise tümünü temizle
|
|
441
|
+
const firstChild = editor.firstChild;
|
|
442
|
+
if (firstChild && firstChild.classList && firstChild.classList.contains('cm-mention-tag')) {
|
|
443
|
+
editor.innerHTML = '';
|
|
444
|
+
}
|
|
445
|
+
editor.dataset.placeholder = 'Yorum yaz...';
|
|
446
|
+
this._closeMentionDropdown();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
document.getElementById('cmBtnSendYorum').addEventListener('click', () => this.sendYorum());
|
|
450
|
+
|
|
451
|
+
// Mention autocomplete - ana editör
|
|
452
|
+
const editor = document.getElementById('cmYorumInput');
|
|
453
|
+
const dd = document.getElementById('cmMentionDropdown');
|
|
454
|
+
this._attachMentionListeners(editor, dd);
|
|
455
|
+
// Enter to send (Shift+Enter = newline)
|
|
456
|
+
editor.addEventListener('keydown', (e) => {
|
|
457
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
458
|
+
if (this._currentDropdown?.children.length > 0 && this._currentDropdown?.style.display !== 'none') return;
|
|
459
|
+
e.preventDefault();
|
|
460
|
+
this.sendYorum();
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
// Modal açıkken sayfanın scroll pozisyonunu hatırla
|
|
466
|
+
_scrollY: 0,
|
|
467
|
+
_mentionQuery: null,
|
|
468
|
+
_mentionSearchTimer: null,
|
|
469
|
+
_currentEditor: null,
|
|
470
|
+
_currentDropdown: null,
|
|
471
|
+
|
|
472
|
+
open(soruId) {
|
|
473
|
+
this.init();
|
|
474
|
+
if (this.aktifSoruId !== soruId) {
|
|
475
|
+
this.aktifSoruId = soruId;
|
|
476
|
+
this.yorumlarYuklendi = false;
|
|
477
|
+
document.getElementById('cmBody').innerHTML = '<div class="sq-yorumlar-loading"><i class="fa-solid fa-spinner fa-spin"></i> Yükleniyor...</div>';
|
|
478
|
+
const editor = document.getElementById('cmYorumInput');
|
|
479
|
+
if (editor) editor.innerHTML = '';
|
|
480
|
+
document.getElementById('cmBtnCancelReply')?.click();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
this.modal.style.display = 'flex';
|
|
484
|
+
// Force reflow for animation
|
|
485
|
+
void this.modal.offsetWidth;
|
|
486
|
+
this.modal.classList.add('show');
|
|
487
|
+
|
|
488
|
+
// iOS Safari dahil tüm mobillerde arka planı kilitle
|
|
489
|
+
this._scrollY = window.scrollY;
|
|
490
|
+
document.body.style.position = 'fixed';
|
|
491
|
+
document.body.style.top = `-${this._scrollY}px`;
|
|
492
|
+
document.body.style.width = '100%';
|
|
493
|
+
document.body.style.overflow = 'hidden';
|
|
494
|
+
|
|
495
|
+
if (!this.yorumlarYuklendi) {
|
|
496
|
+
this.loadYorumlar();
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
close() {
|
|
501
|
+
if (!this.modal) return;
|
|
502
|
+
this.modal.classList.remove('show');
|
|
503
|
+
|
|
504
|
+
// Body kilidini aç ve scroll pozisyonunu geri yükle
|
|
505
|
+
document.body.style.position = '';
|
|
506
|
+
document.body.style.top = '';
|
|
507
|
+
document.body.style.width = '';
|
|
508
|
+
document.body.style.overflow = '';
|
|
509
|
+
window.scrollTo(0, this._scrollY);
|
|
510
|
+
|
|
511
|
+
setTimeout(() => {
|
|
512
|
+
this.modal.style.display = 'none';
|
|
513
|
+
}, 300);
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
async loadYorumlar() {
|
|
517
|
+
const body = document.getElementById('cmBody');
|
|
518
|
+
try {
|
|
519
|
+
const res = await window.apiPost('/api/switch-case/yorumSoru', {
|
|
520
|
+
action: 'getList',
|
|
521
|
+
soruId: this.aktifSoruId,
|
|
522
|
+
limit: 100
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
if (res.status === 'success') {
|
|
526
|
+
this.yorumlarYuklendi = true;
|
|
527
|
+
const yorumlar = res.data;
|
|
528
|
+
if (yorumlar.length === 0) {
|
|
529
|
+
body.innerHTML = '<div class="sq-yorumlar-loading">İlk yorumu sen yaz!</div>';
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const parents = yorumlar.filter(y => !y.ustYorumId);
|
|
534
|
+
const children = yorumlar.filter(y => y.ustYorumId);
|
|
535
|
+
|
|
536
|
+
let html = '<div class="sq-yorumlar-list">';
|
|
537
|
+
for (const p of parents) {
|
|
538
|
+
html += this.renderYorum(p, false);
|
|
539
|
+
const replies = children.filter(c => c.ustYorumId === p.yorumId);
|
|
540
|
+
if (replies.length > 0) {
|
|
541
|
+
html += `<button class="sq-toggle-replies" data-target="replies-${p.yorumId}">Yanıtları gör (${replies.length})</button>`;
|
|
542
|
+
html += `<div class="sq-replies-container" id="replies-${p.yorumId}">`;
|
|
543
|
+
for (const r of replies) {
|
|
544
|
+
html += this.renderYorum(r, true);
|
|
545
|
+
}
|
|
546
|
+
html += `</div>`;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
html += '</div>';
|
|
550
|
+
body.innerHTML = html;
|
|
551
|
+
this.attachEvents();
|
|
552
|
+
} else {
|
|
553
|
+
body.innerHTML = '<div class="sq-yorumlar-loading" style="color:red">Yorumlar yüklenemedi.</div>';
|
|
554
|
+
}
|
|
555
|
+
} catch (e) {
|
|
556
|
+
body.innerHTML = '<div class="sq-yorumlar-loading" style="color:red">Bağlantı hatası.</div>';
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
|
|
560
|
+
getRoleLabel(rol) {
|
|
561
|
+
if (rol == 0) return '<i class="fa-solid fa-shield-halved"></i> Moderatör';
|
|
562
|
+
if (rol == 1) return '<i class="fa-solid fa-user"></i> Aday';
|
|
563
|
+
if (rol == 2) return '<i class="fa-solid fa-chalkboard-user"></i> Öğretmen';
|
|
564
|
+
if (rol == 3) return '<i class="fa-solid fa-book"></i> Yayınevi';
|
|
565
|
+
if (rol == 4) return '<i class="fa-solid fa-graduation-cap"></i> Mentor';
|
|
566
|
+
return '';
|
|
567
|
+
},
|
|
568
|
+
|
|
569
|
+
timeAgoJs(unixTS) {
|
|
570
|
+
const diff = Math.floor(Date.now() / 1000) - unixTS;
|
|
571
|
+
if (diff < 60) return 'Az önce';
|
|
572
|
+
if (diff < 3600) return Math.floor(diff / 60) + 'd önce';
|
|
573
|
+
if (diff < 86400) return Math.floor(diff / 3600) + 's önce';
|
|
574
|
+
if (diff < 604800) return Math.floor(diff / 86400) + 'g önce';
|
|
575
|
+
|
|
576
|
+
const d = new Date(unixTS * 1000);
|
|
577
|
+
return d.toLocaleDateString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
578
|
+
},
|
|
579
|
+
|
|
580
|
+
renderYorum(y, isReply = false) {
|
|
581
|
+
const avatar = y.profilFotografi
|
|
582
|
+
? (y.profilFotografi.startsWith('http') ? y.profilFotografi : '/uploads/profiles/' + y.profilFotografi)
|
|
583
|
+
: `https://sinavortagim.artilingo.com/system/default-avatar.png`;
|
|
584
|
+
|
|
585
|
+
return `
|
|
586
|
+
<div class="sq-yorum-item ${isReply ? 'sq-yorum-reply' : ''}" id="yorum-${y.yorumId}">
|
|
587
|
+
<a href="/user/profil/${window.escHtml(y.kullaniciAdi)}">
|
|
588
|
+
${y.rol == 0 ? `
|
|
589
|
+
<div class="is-mod-avatar" style="width: 35px; height: 35px; margin-right: 0.5rem;">
|
|
590
|
+
<img src="${window.escHtml(avatar)}" class="sq-avatar" alt="Avatar" style="margin: 0; width:100%; height:100%;">
|
|
591
|
+
</div>
|
|
592
|
+
` : `
|
|
593
|
+
<img src="${window.escHtml(avatar)}" class="sq-avatar" alt="Avatar">
|
|
594
|
+
`}
|
|
595
|
+
</a>
|
|
596
|
+
<div class="sq-yorum-bubble">
|
|
597
|
+
<div class="sq-yorum-header">
|
|
598
|
+
<a href="/user/profil/${window.escHtml(y.kullaniciAdi)}" style="text-decoration: none; color: inherit;">
|
|
599
|
+
<strong class="${y.rol == 0 ? 'is-mod-text' : ''}">${y.adSoyad ? window.escHtml(y.adSoyad) : '@' + window.escHtml(y.kullaniciAdi)}</strong>
|
|
600
|
+
</a>
|
|
601
|
+
${y.rol == 0 ? `<span class="badge-mod" style="font-size: 0.75rem; color: #a855f7; margin-left: 5px;"><i class="fa-solid fa-shield-halved"></i> Moderatör</span>` : ''}
|
|
602
|
+
${!y.adSoyad ? '' : `<span>@${window.escHtml(y.kullaniciAdi)}</span>`}
|
|
603
|
+
${y.rol != 0 ? `<span class="sq-yorum-role">${this.getRoleLabel(y.rol)}</span>` : ''}
|
|
604
|
+
<span>${this.timeAgoJs(y.olusturulmaTarihi)}</span>
|
|
605
|
+
</div>
|
|
606
|
+
<div class="sq-yorum-metin">${this._renderMentions(y.yorumMetni).replace(/\n/g, '<br>')}</div>
|
|
607
|
+
<div class="sq-yorum-footer">
|
|
608
|
+
${!isReply ? `<button class="sq-yorum-action btn-reply" data-id="${y.yorumId}" data-user="@${window.escHtml(y.kullaniciAdi)}"><i class="fa-solid fa-reply"></i> Yanıtla</button>` : ''}
|
|
609
|
+
${y.canEdit ? `<button class="sq-yorum-action edit btn-edit-yorum" data-id="${y.yorumId}" data-metin="${window.escHtml(y.yorumMetni)}"><i class="fa-solid fa-pen"></i> Düzenle</button>` : ''}
|
|
610
|
+
${y.canDelete ? `<button class="sq-yorum-action delete btn-delete-yorum" data-id="${y.yorumId}"><i class="fa-solid fa-trash"></i> Sil</button>` : ''}
|
|
611
|
+
${!y.canEdit ? `<button class="sq-yorum-action text-danger" onclick="reportContent(3, '${y.yorumId}')"><i class="fa-solid fa-flag"></i> Raporla</button>` : ''}
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
</div>
|
|
615
|
+
`;
|
|
616
|
+
},
|
|
617
|
+
|
|
618
|
+
attachEvents() {
|
|
619
|
+
const _this = this;
|
|
620
|
+
document.querySelectorAll('.btn-reply').forEach(btn => {
|
|
621
|
+
btn.addEventListener('click', function() {
|
|
622
|
+
const replyTo = this.dataset.user; // '@kullaniciAdi'
|
|
623
|
+
_this.aktifUstYorumId = this.dataset.id;
|
|
624
|
+
document.getElementById('cmReplyToUsername').textContent = replyTo;
|
|
625
|
+
document.getElementById('cmReplyStatus').classList.add('active');
|
|
626
|
+
|
|
627
|
+
// Editor'a @mention tag'i ekle
|
|
628
|
+
const editor = document.getElementById('cmYorumInput');
|
|
629
|
+
editor.innerHTML = '';
|
|
630
|
+
const tag = _this._createMentionTag(replyTo.replace('@', ''));
|
|
631
|
+
editor.appendChild(tag);
|
|
632
|
+
// tag sonrasına boflukluk ekle ve kursörü yerleştir
|
|
633
|
+
const space = document.createTextNode('\u00a0');
|
|
634
|
+
editor.appendChild(space);
|
|
635
|
+
_this._placeCursorAfter(space);
|
|
636
|
+
editor.dataset.placeholder = 'Yanıtını yaz...';
|
|
637
|
+
editor.focus();
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
document.querySelectorAll('.sq-toggle-replies').forEach(btn => {
|
|
642
|
+
btn.addEventListener('click', function() {
|
|
643
|
+
const targetId = this.dataset.target;
|
|
644
|
+
const container = document.getElementById(targetId);
|
|
645
|
+
if (container.classList.contains('open')) {
|
|
646
|
+
container.classList.remove('open');
|
|
647
|
+
this.textContent = `Yanıtları gör (${container.children.length})`;
|
|
648
|
+
} else {
|
|
649
|
+
container.classList.add('open');
|
|
650
|
+
this.textContent = `Yanıtları gizle`;
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
document.querySelectorAll('.btn-delete-yorum').forEach(btn => {
|
|
656
|
+
btn.addEventListener('click', function() {
|
|
657
|
+
window.showConfirm('Yorumu Sil', 'Bu yorumu silmek istediğine emin misin?', { confirmClass: 'btn-danger', confirmText: 'Sil' })
|
|
658
|
+
.then(async (confirmed) => {
|
|
659
|
+
if (!confirmed) return;
|
|
660
|
+
|
|
661
|
+
const id = this.dataset.id;
|
|
662
|
+
const res = await window.apiPost('/api/switch-case/yorumSoru', {
|
|
663
|
+
action: 'delete',
|
|
664
|
+
soruId: _this.aktifSoruId,
|
|
665
|
+
yorumId: id
|
|
666
|
+
});
|
|
667
|
+
if (res.status === 'success') {
|
|
668
|
+
window.showToast('Yorum silindi.');
|
|
669
|
+
_this.updateYorumCountUI(res.data?.yeniYorumSayisi);
|
|
670
|
+
_this.loadYorumlar();
|
|
671
|
+
} else {
|
|
672
|
+
window.showToast(res.message, 'error');
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
document.querySelectorAll('.btn-edit-yorum').forEach(btn => {
|
|
679
|
+
btn.addEventListener('click', function() {
|
|
680
|
+
const yorumId = this.dataset.id;
|
|
681
|
+
const mevcutMetin = this.dataset.metin;
|
|
682
|
+
const bubble = this.closest('.sq-yorum-bubble');
|
|
683
|
+
const metinEl = bubble.querySelector('.sq-yorum-metin');
|
|
684
|
+
const footerEl = bubble.querySelector('.sq-yorum-footer');
|
|
685
|
+
|
|
686
|
+
// Zaten düzenleme modunda ise çık
|
|
687
|
+
if (bubble.querySelector('.sq-edit-area')) return;
|
|
688
|
+
|
|
689
|
+
// Inline edit alanı oluştur
|
|
690
|
+
const editArea = document.createElement('div');
|
|
691
|
+
editArea.className = 'sq-edit-area';
|
|
692
|
+
editArea.innerHTML = `
|
|
693
|
+
<div class="cm-mention-dropdown sq-edit-mention-dropdown"></div>
|
|
694
|
+
<div class="sq-edit-editor cm-editor sq-edit-textarea"
|
|
695
|
+
contenteditable="true"
|
|
696
|
+
data-placeholder="Yorumu düzenle..."
|
|
697
|
+
role="textbox"
|
|
698
|
+
aria-multiline="true"
|
|
699
|
+
spellcheck="true"></div>
|
|
700
|
+
<div class="sq-edit-actions">
|
|
701
|
+
<button class="sq-yorum-action edit sq-edit-save"><i class="fa-solid fa-check"></i> Kaydet</button>
|
|
702
|
+
<button class="sq-yorum-action sq-edit-cancel"><i class="fa-solid fa-xmark"></i> İptal</button>
|
|
703
|
+
</div>
|
|
704
|
+
`;
|
|
705
|
+
|
|
706
|
+
metinEl.style.display = 'none';
|
|
707
|
+
footerEl.style.display = 'none';
|
|
708
|
+
bubble.appendChild(editArea);
|
|
709
|
+
|
|
710
|
+
const editEditorEl = editArea.querySelector('.sq-edit-editor');
|
|
711
|
+
const editDropdownEl = editArea.querySelector('.sq-edit-mention-dropdown');
|
|
712
|
+
|
|
713
|
+
// Mevcut metni editöre yükle
|
|
714
|
+
editEditorEl.textContent = mevcutMetin;
|
|
715
|
+
_this._attachMentionListeners(editEditorEl, editDropdownEl);
|
|
716
|
+
editEditorEl.focus();
|
|
717
|
+
// Kursörü sona taşı
|
|
718
|
+
const selRange = document.createRange();
|
|
719
|
+
selRange.selectNodeContents(editEditorEl);
|
|
720
|
+
selRange.collapse(false);
|
|
721
|
+
window.getSelection().removeAllRanges();
|
|
722
|
+
window.getSelection().addRange(selRange);
|
|
723
|
+
|
|
724
|
+
// İptal
|
|
725
|
+
editArea.querySelector('.sq-edit-cancel').addEventListener('click', () => {
|
|
726
|
+
editArea.remove();
|
|
727
|
+
metinEl.style.display = '';
|
|
728
|
+
footerEl.style.display = '';
|
|
729
|
+
// Ana editöre geri dön
|
|
730
|
+
const mainEditor = document.getElementById('cmYorumInput');
|
|
731
|
+
const mainDd = document.getElementById('cmMentionDropdown');
|
|
732
|
+
if (mainEditor) { _this._currentEditor = mainEditor; _this._currentDropdown = mainDd; }
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// Kaydet
|
|
736
|
+
editArea.querySelector('.sq-edit-save').addEventListener('click', async () => {
|
|
737
|
+
const yeniMetin = _this._getEditorText(editEditorEl).trim();
|
|
738
|
+
if (!yeniMetin) return;
|
|
739
|
+
|
|
740
|
+
const saveBtn = editArea.querySelector('.sq-edit-save');
|
|
741
|
+
saveBtn.disabled = true;
|
|
742
|
+
saveBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
|
|
743
|
+
|
|
744
|
+
const res = await window.apiPost('/api/switch-case/yorumSoru', {
|
|
745
|
+
action: 'update',
|
|
746
|
+
yorumId: yorumId,
|
|
747
|
+
yorumMetni: yeniMetin
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
if (res.status === 'success') {
|
|
751
|
+
// DOM'u anlık güncelle
|
|
752
|
+
metinEl.innerHTML = _this._renderMentions(res.data.yeniMetin).replace(/\n/g, '<br>');
|
|
753
|
+
btn.dataset.metin = res.data.yeniMetin;
|
|
754
|
+
editArea.remove();
|
|
755
|
+
metinEl.style.display = '';
|
|
756
|
+
footerEl.style.display = '';
|
|
757
|
+
// Ana editöre geri dön
|
|
758
|
+
const mainEditor = document.getElementById('cmYorumInput');
|
|
759
|
+
const mainDd = document.getElementById('cmMentionDropdown');
|
|
760
|
+
if (mainEditor) { _this._currentEditor = mainEditor; _this._currentDropdown = mainDd; }
|
|
761
|
+
window.showToast('Yorum güncellendi.');
|
|
762
|
+
} else {
|
|
763
|
+
window.showToast(res.message || 'Güncellenemedi.', 'error');
|
|
764
|
+
saveBtn.disabled = false;
|
|
765
|
+
saveBtn.innerHTML = '<i class="fa-solid fa-check"></i> Kaydet';
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
},
|
|
771
|
+
|
|
772
|
+
updateYorumCountUI(count) {
|
|
773
|
+
if (count !== undefined) {
|
|
774
|
+
// Update on details page if available
|
|
775
|
+
document.querySelectorAll('.sq-yorum-count, .sd-action-btn[data-soru-id="'+this.aktifSoruId+'"] span').forEach(el => {
|
|
776
|
+
el.textContent = count;
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
|
|
781
|
+
async sendYorum() {
|
|
782
|
+
const editor = document.getElementById('cmYorumInput');
|
|
783
|
+
const btn = document.getElementById('cmBtnSendYorum');
|
|
784
|
+
const metin = this._getEditorText(editor).trim();
|
|
785
|
+
if (!metin) return;
|
|
786
|
+
|
|
787
|
+
btn.disabled = true;
|
|
788
|
+
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
|
|
789
|
+
|
|
790
|
+
const res = await window.apiPost('/api/switch-case/yorumSoru', {
|
|
791
|
+
action: 'create',
|
|
792
|
+
soruId: this.aktifSoruId,
|
|
793
|
+
yorumMetni: metin,
|
|
794
|
+
ustYorumId: this.aktifUstYorumId || ''
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
btn.disabled = false;
|
|
798
|
+
btn.innerHTML = '<i class="fa-solid fa-paper-plane"></i>';
|
|
799
|
+
|
|
800
|
+
if (res.status === 'success') {
|
|
801
|
+
document.getElementById('cmBtnCancelReply').click();
|
|
802
|
+
editor.innerHTML = '';
|
|
803
|
+
this.updateYorumCountUI(res.data?.yeniYorumSayisi);
|
|
804
|
+
this.loadYorumlar();
|
|
805
|
+
setTimeout(() => {
|
|
806
|
+
const body = document.getElementById('cmBody');
|
|
807
|
+
body.scrollTop = body.scrollHeight;
|
|
808
|
+
}, 300);
|
|
809
|
+
} else {
|
|
810
|
+
window.showToast(res.message || 'Gönderilemedi', 'error');
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
|
|
814
|
+
/* ---- Mention (Etiket) Sistemi ---- */
|
|
815
|
+
|
|
816
|
+
/** Editor'daki tüm metni (mention tag'leri dahil) düz string olarak al */
|
|
817
|
+
_getEditorText(editor) {
|
|
818
|
+
let text = '';
|
|
819
|
+
editor.childNodes.forEach(node => {
|
|
820
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
821
|
+
text += node.textContent;
|
|
822
|
+
} else if (node.classList && node.classList.contains('cm-mention-tag')) {
|
|
823
|
+
text += '@' + node.dataset.username;
|
|
824
|
+
} else {
|
|
825
|
+
text += node.textContent;
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
return text;
|
|
829
|
+
},
|
|
830
|
+
|
|
831
|
+
/** Yorum metnindeki @kullaniciAdi'lerini mor tıklanabilir linke dönüştür */
|
|
832
|
+
_renderMentions(text) {
|
|
833
|
+
return window.escHtml(text)
|
|
834
|
+
.replace(/@([a-zA-Z0-9_\.]{1,30})/g,
|
|
835
|
+
'<a href="/user/profil/$1" class="cm-mention-link">@$1</a>');
|
|
836
|
+
},
|
|
837
|
+
|
|
838
|
+
/** Mention tag span'i oluştur */
|
|
839
|
+
_createMentionTag(username) {
|
|
840
|
+
const span = document.createElement('span');
|
|
841
|
+
span.className = 'cm-mention-tag';
|
|
842
|
+
span.contentEditable = 'false';
|
|
843
|
+
span.dataset.username = username;
|
|
844
|
+
span.textContent = '@' + username;
|
|
845
|
+
return span;
|
|
846
|
+
},
|
|
847
|
+
|
|
848
|
+
/** Kursörü bir node sonrasına taşı */
|
|
849
|
+
_placeCursorAfter(node) {
|
|
850
|
+
const sel = window.getSelection();
|
|
851
|
+
const range = document.createRange();
|
|
852
|
+
range.setStartAfter(node);
|
|
853
|
+
range.collapse(true);
|
|
854
|
+
sel.removeAllRanges();
|
|
855
|
+
sel.addRange(range);
|
|
856
|
+
},
|
|
857
|
+
|
|
858
|
+
_attachMentionListeners(editorEl, dropdownEl) {
|
|
859
|
+
editorEl.addEventListener('focus', () => {
|
|
860
|
+
this._currentEditor = editorEl;
|
|
861
|
+
this._currentDropdown = dropdownEl;
|
|
862
|
+
});
|
|
863
|
+
editorEl.addEventListener('input', () => this._onEditorInput());
|
|
864
|
+
editorEl.addEventListener('keydown', (e) => this._onEditorKeydown(e));
|
|
865
|
+
// İlk bağlamada hemen aktif yap
|
|
866
|
+
this._currentEditor = editorEl;
|
|
867
|
+
this._currentDropdown = dropdownEl;
|
|
868
|
+
},
|
|
869
|
+
|
|
870
|
+
_closeMentionDropdown() {
|
|
871
|
+
const dd = this._currentDropdown;
|
|
872
|
+
if (dd) { dd.innerHTML = ''; dd.style.display = 'none'; }
|
|
873
|
+
this._mentionQuery = null;
|
|
874
|
+
},
|
|
875
|
+
|
|
876
|
+
_onEditorInput() {
|
|
877
|
+
const sel = window.getSelection();
|
|
878
|
+
if (!sel.rangeCount) return;
|
|
879
|
+
|
|
880
|
+
// Kursörün solundaki metni al
|
|
881
|
+
const range = sel.getRangeAt(0);
|
|
882
|
+
const textBefore = range.startContainer.textContent?.slice(0, range.startOffset) || '';
|
|
883
|
+
|
|
884
|
+
// Son @... patternını ara
|
|
885
|
+
const match = textBefore.match(/@([a-zA-Z0-9_\.]*)$/);
|
|
886
|
+
if (match && match[1].length >= 3) {
|
|
887
|
+
const query = match[1];
|
|
888
|
+
if (query !== this._mentionQuery) {
|
|
889
|
+
this._mentionQuery = query;
|
|
890
|
+
clearTimeout(this._mentionSearchTimer);
|
|
891
|
+
this._mentionSearchTimer = setTimeout(() => this._fetchMentionSuggestions(query), 300);
|
|
892
|
+
}
|
|
893
|
+
} else {
|
|
894
|
+
this._closeMentionDropdown();
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
|
|
898
|
+
_onEditorKeydown(e) {
|
|
899
|
+
const dd = document.getElementById('cmMentionDropdown');
|
|
900
|
+
const items = dd?.querySelectorAll('.cm-mention-item');
|
|
901
|
+
if (!items || items.length === 0 || dd.style.display === 'none') return;
|
|
902
|
+
|
|
903
|
+
const active = dd.querySelector('.cm-mention-item.active');
|
|
904
|
+
if (e.key === 'ArrowDown') {
|
|
905
|
+
e.preventDefault();
|
|
906
|
+
const next = active ? active.nextElementSibling : items[0];
|
|
907
|
+
active?.classList.remove('active');
|
|
908
|
+
next?.classList.add('active');
|
|
909
|
+
} else if (e.key === 'ArrowUp') {
|
|
910
|
+
e.preventDefault();
|
|
911
|
+
const prev = active ? active.previousElementSibling : items[items.length - 1];
|
|
912
|
+
active?.classList.remove('active');
|
|
913
|
+
prev?.classList.add('active');
|
|
914
|
+
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
915
|
+
if (active) {
|
|
916
|
+
e.preventDefault();
|
|
917
|
+
this._insertMention(active.dataset.username);
|
|
918
|
+
}
|
|
919
|
+
} else if (e.key === 'Escape') {
|
|
920
|
+
this._closeMentionDropdown();
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
async _fetchMentionSuggestions(query) {
|
|
925
|
+
try {
|
|
926
|
+
const res = await window.apiPost('/api/switch-case/search', {
|
|
927
|
+
action: 'search',
|
|
928
|
+
keyword: query
|
|
929
|
+
});
|
|
930
|
+
if (res.status === 'success') {
|
|
931
|
+
this._showMentionDropdown(res.data.slice(0, 5));
|
|
932
|
+
}
|
|
933
|
+
} catch (e) { /* sessiz hata */ }
|
|
934
|
+
},
|
|
935
|
+
|
|
936
|
+
_showMentionDropdown(users) {
|
|
937
|
+
const dd = this._currentDropdown;
|
|
938
|
+
if (!users || users.length === 0) { this._closeMentionDropdown(); return; }
|
|
939
|
+
|
|
940
|
+
dd.innerHTML = users.map(u => {
|
|
941
|
+
const avatar = u.profilFotografi
|
|
942
|
+
? (u.profilFotografi.startsWith('http') ? u.profilFotografi : '/uploads/profiles/' + u.profilFotografi)
|
|
943
|
+
: `https://sinavortagim.artilingo.com/system/default-avatar.png`;
|
|
944
|
+
return `
|
|
945
|
+
<div class="cm-mention-item" data-username="${window.escHtml(u.kullaniciAdi)}">
|
|
946
|
+
<img src="${window.escHtml(avatar)}" alt="">
|
|
947
|
+
<div class="cm-mention-info">
|
|
948
|
+
<strong>${window.escHtml(u.adSoyad || u.kullaniciAdi)}</strong>
|
|
949
|
+
<span>@${window.escHtml(u.kullaniciAdi)}</span>
|
|
950
|
+
</div>
|
|
951
|
+
</div>`;
|
|
952
|
+
}).join('');
|
|
953
|
+
dd.style.display = 'block';
|
|
954
|
+
|
|
955
|
+
dd.querySelectorAll('.cm-mention-item').forEach(item => {
|
|
956
|
+
item.addEventListener('mousedown', (e) => {
|
|
957
|
+
e.preventDefault();
|
|
958
|
+
this._insertMention(item.dataset.username);
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
},
|
|
962
|
+
|
|
963
|
+
_insertMention(username) {
|
|
964
|
+
const editor = this._currentEditor;
|
|
965
|
+
const sel = window.getSelection();
|
|
966
|
+
if (!sel.rangeCount || !editor) return;
|
|
967
|
+
|
|
968
|
+
const range = sel.getRangeAt(0);
|
|
969
|
+
const node = range.startContainer;
|
|
970
|
+
const text = node.textContent || '';
|
|
971
|
+
const offset = range.startOffset;
|
|
972
|
+
|
|
973
|
+
// @ ile başlayan kısmı sil
|
|
974
|
+
const atIdx = text.lastIndexOf('@', offset - 1);
|
|
975
|
+
if (atIdx !== -1) {
|
|
976
|
+
node.textContent = text.slice(0, atIdx) + text.slice(offset);
|
|
977
|
+
range.setStart(node, atIdx);
|
|
978
|
+
range.collapse(true);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Mention span ekle
|
|
982
|
+
const tag = this._createMentionTag(username);
|
|
983
|
+
range.insertNode(tag);
|
|
984
|
+
|
|
985
|
+
// Tag sonrasına boşluk ekle ve kursörü oraya taşı
|
|
986
|
+
const space = document.createTextNode('\u00a0');
|
|
987
|
+
tag.parentNode.insertBefore(space, tag.nextSibling);
|
|
988
|
+
this._placeCursorAfter(space);
|
|
989
|
+
|
|
990
|
+
this._closeMentionDropdown();
|
|
991
|
+
editor.focus();
|
|
992
|
+
},
|
|
993
|
+
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
/* --- Global Gonderi Comment Manager --- */
|
|
997
|
+
window.GonderiCommentManager = {
|
|
998
|
+
modal: null,
|
|
999
|
+
aktifGonderiId: null,
|
|
1000
|
+
aktifUstYorumId: null,
|
|
1001
|
+
yorumlarYuklendi: false,
|
|
1002
|
+
|
|
1003
|
+
init() {
|
|
1004
|
+
if (this.modal) return;
|
|
1005
|
+
const html = `
|
|
1006
|
+
<div class="cm-overlay" id="gcmOverlay">
|
|
1007
|
+
<div class="cm-modal">
|
|
1008
|
+
<div class="cm-header">
|
|
1009
|
+
<h2>Yorumlar</h2>
|
|
1010
|
+
<button class="cm-close" id="gcmClose"><i class="fa-solid fa-xmark"></i></button>
|
|
1011
|
+
</div>
|
|
1012
|
+
<div class="cm-body" id="gcmBody">
|
|
1013
|
+
<div class="sq-yorumlar-loading"><i class="fa-solid fa-spinner fa-spin"></i> Yükleniyor...</div>
|
|
1014
|
+
</div>
|
|
1015
|
+
<div class="cm-footer">
|
|
1016
|
+
<div class="sq-yorum-input-wrap">
|
|
1017
|
+
<div class="sq-reply-status" id="gcmReplyStatus">
|
|
1018
|
+
<span>Yanıtlanıyor: <strong id="gcmReplyToUsername"></strong></span>
|
|
1019
|
+
<button type="button" class="sq-cancel-reply" id="gcmBtnCancelReply"><i class="fa-solid fa-xmark"></i> İptal</button>
|
|
1020
|
+
</div>
|
|
1021
|
+
<!-- Mention Autocomplete Dropdown -->
|
|
1022
|
+
<div class="cm-mention-dropdown" id="gcmMentionDropdown"></div>
|
|
1023
|
+
<div class="sq-yorum-input-row">
|
|
1024
|
+
<div class="sq-yorum-input cm-editor"
|
|
1025
|
+
id="gcmYorumInput"
|
|
1026
|
+
contenteditable="true"
|
|
1027
|
+
data-placeholder="Yorum yaz..."
|
|
1028
|
+
role="textbox"
|
|
1029
|
+
aria-multiline="true"
|
|
1030
|
+
spellcheck="true"></div>
|
|
1031
|
+
<button class="btn btn-primary btn-sm" id="cmBtnSendYorum">
|
|
1032
|
+
<i class="fa-solid fa-paper-plane"></i>
|
|
1033
|
+
</button>
|
|
1034
|
+
</div>
|
|
1035
|
+
</div>
|
|
1036
|
+
</div>
|
|
1037
|
+
</div>
|
|
1038
|
+
</div>
|
|
1039
|
+
`;
|
|
1040
|
+
document.body.insertAdjacentHTML('beforeend', html);
|
|
1041
|
+
this.modal = document.getElementById('gcmOverlay');
|
|
1042
|
+
|
|
1043
|
+
document.getElementById('gcmClose').addEventListener('click', () => this.close());
|
|
1044
|
+
this.modal.addEventListener('click', (e) => {
|
|
1045
|
+
if (e.target === this.modal) this.close();
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
document.getElementById('gcmBtnCancelReply').addEventListener('click', () => {
|
|
1049
|
+
this.aktifUstYorumId = null;
|
|
1050
|
+
document.getElementById('gcmReplyStatus').classList.remove('active');
|
|
1051
|
+
const editor = document.getElementById('gcmYorumInput');
|
|
1052
|
+
// @mention tagini sil, sadece tag ise tümünü temizle
|
|
1053
|
+
const firstChild = editor.firstChild;
|
|
1054
|
+
if (firstChild && firstChild.classList && firstChild.classList.contains('cm-mention-tag')) {
|
|
1055
|
+
editor.innerHTML = '';
|
|
1056
|
+
}
|
|
1057
|
+
editor.dataset.placeholder = 'Yorum yaz...';
|
|
1058
|
+
this._closeMentionDropdown();
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
document.getElementById('cmBtnSendYorum').addEventListener('click', () => this.sendYorum());
|
|
1062
|
+
|
|
1063
|
+
// Mention autocomplete - ana editör
|
|
1064
|
+
const editor = document.getElementById('gcmYorumInput');
|
|
1065
|
+
const dd = document.getElementById('gcmMentionDropdown');
|
|
1066
|
+
this._attachMentionListeners(editor, dd);
|
|
1067
|
+
// Enter to send (Shift+Enter = newline)
|
|
1068
|
+
editor.addEventListener('keydown', (e) => {
|
|
1069
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1070
|
+
if (this._currentDropdown?.children.length > 0 && this._currentDropdown?.style.display !== 'none') return;
|
|
1071
|
+
e.preventDefault();
|
|
1072
|
+
this.sendYorum();
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
},
|
|
1076
|
+
|
|
1077
|
+
// Modal açıkken sayfanın scroll pozisyonunu hatırla
|
|
1078
|
+
_scrollY: 0,
|
|
1079
|
+
_mentionQuery: null,
|
|
1080
|
+
_mentionSearchTimer: null,
|
|
1081
|
+
_currentEditor: null,
|
|
1082
|
+
_currentDropdown: null,
|
|
1083
|
+
|
|
1084
|
+
open(gonderiId) {
|
|
1085
|
+
this.init();
|
|
1086
|
+
if (this.aktifGonderiId !== gonderiId) {
|
|
1087
|
+
this.aktifGonderiId = gonderiId;
|
|
1088
|
+
this.yorumlarYuklendi = false;
|
|
1089
|
+
document.getElementById('gcmBody').innerHTML = '<div class="sq-yorumlar-loading"><i class="fa-solid fa-spinner fa-spin"></i> Yükleniyor...</div>';
|
|
1090
|
+
const editor = document.getElementById('gcmYorumInput');
|
|
1091
|
+
if (editor) editor.innerHTML = '';
|
|
1092
|
+
document.getElementById('gcmBtnCancelReply')?.click();
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
this.modal.style.display = 'flex';
|
|
1096
|
+
// Force reflow for animation
|
|
1097
|
+
void this.modal.offsetWidth;
|
|
1098
|
+
this.modal.classList.add('show');
|
|
1099
|
+
|
|
1100
|
+
// iOS Safari dahil tüm mobillerde arka planı kilitle
|
|
1101
|
+
this._scrollY = window.scrollY;
|
|
1102
|
+
document.body.style.position = 'fixed';
|
|
1103
|
+
document.body.style.top = `-${this._scrollY}px`;
|
|
1104
|
+
document.body.style.width = '100%';
|
|
1105
|
+
document.body.style.overflow = 'hidden';
|
|
1106
|
+
|
|
1107
|
+
if (!this.yorumlarYuklendi) {
|
|
1108
|
+
this.loadYorumlar();
|
|
1109
|
+
}
|
|
1110
|
+
},
|
|
1111
|
+
|
|
1112
|
+
close() {
|
|
1113
|
+
if (!this.modal) return;
|
|
1114
|
+
this.modal.classList.remove('show');
|
|
1115
|
+
|
|
1116
|
+
// Body kilidini aç ve scroll pozisyonunu geri yükle
|
|
1117
|
+
document.body.style.position = '';
|
|
1118
|
+
document.body.style.top = '';
|
|
1119
|
+
document.body.style.width = '';
|
|
1120
|
+
document.body.style.overflow = '';
|
|
1121
|
+
window.scrollTo(0, this._scrollY);
|
|
1122
|
+
|
|
1123
|
+
setTimeout(() => {
|
|
1124
|
+
this.modal.style.display = 'none';
|
|
1125
|
+
}, 300);
|
|
1126
|
+
},
|
|
1127
|
+
|
|
1128
|
+
async loadYorumlar() {
|
|
1129
|
+
const body = document.getElementById('gcmBody');
|
|
1130
|
+
try {
|
|
1131
|
+
const res = await window.apiPost('/api/switch-case/yorumGonderi', {
|
|
1132
|
+
action: 'getList',
|
|
1133
|
+
gonderiId: this.aktifGonderiId,
|
|
1134
|
+
limit: 100
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
if (res.status === 'success') {
|
|
1138
|
+
this.yorumlarYuklendi = true;
|
|
1139
|
+
const yorumlar = res.data;
|
|
1140
|
+
if (yorumlar.length === 0) {
|
|
1141
|
+
body.innerHTML = '<div class="sq-yorumlar-loading">İlk yorumu sen yaz!</div>';
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const parents = yorumlar.filter(y => !y.ustYorumId);
|
|
1146
|
+
const children = yorumlar.filter(y => y.ustYorumId);
|
|
1147
|
+
|
|
1148
|
+
let html = '<div class="sq-yorumlar-list">';
|
|
1149
|
+
for (const p of parents) {
|
|
1150
|
+
html += this.renderYorum(p, false);
|
|
1151
|
+
const replies = children.filter(c => c.ustYorumId === p.yorumId);
|
|
1152
|
+
if (replies.length > 0) {
|
|
1153
|
+
html += `<button class="sq-toggle-replies" data-target="replies-${p.yorumId}">Yanıtları gör (${replies.length})</button>`;
|
|
1154
|
+
html += `<div class="sq-replies-container" id="replies-${p.yorumId}">`;
|
|
1155
|
+
for (const r of replies) {
|
|
1156
|
+
html += this.renderYorum(r, true);
|
|
1157
|
+
}
|
|
1158
|
+
html += `</div>`;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
html += '</div>';
|
|
1162
|
+
body.innerHTML = html;
|
|
1163
|
+
this.attachEvents();
|
|
1164
|
+
} else {
|
|
1165
|
+
body.innerHTML = '<div class="sq-yorumlar-loading" style="color:red">Yorumlar yüklenemedi.</div>';
|
|
1166
|
+
}
|
|
1167
|
+
} catch (e) {
|
|
1168
|
+
body.innerHTML = '<div class="sq-yorumlar-loading" style="color:red">Bağlantı hatası.</div>';
|
|
1169
|
+
}
|
|
1170
|
+
},
|
|
1171
|
+
|
|
1172
|
+
getRoleLabel(rol) {
|
|
1173
|
+
if (rol == 0) return '<i class="fa-solid fa-shield-halved"></i> Moderatör';
|
|
1174
|
+
if (rol == 1) return '<i class="fa-solid fa-user"></i> Aday';
|
|
1175
|
+
if (rol == 2) return '<i class="fa-solid fa-chalkboard-user"></i> Öğretmen';
|
|
1176
|
+
if (rol == 3) return '<i class="fa-solid fa-book"></i> Yayınevi';
|
|
1177
|
+
if (rol == 4) return '<i class="fa-solid fa-graduation-cap"></i> Mentor';
|
|
1178
|
+
return '';
|
|
1179
|
+
},
|
|
1180
|
+
|
|
1181
|
+
timeAgoJs(unixTS) {
|
|
1182
|
+
const diff = Math.floor(Date.now() / 1000) - unixTS;
|
|
1183
|
+
if (diff < 60) return 'Az önce';
|
|
1184
|
+
if (diff < 3600) return Math.floor(diff / 60) + 'd önce';
|
|
1185
|
+
if (diff < 86400) return Math.floor(diff / 3600) + 's önce';
|
|
1186
|
+
if (diff < 604800) return Math.floor(diff / 86400) + 'g önce';
|
|
1187
|
+
|
|
1188
|
+
const d = new Date(unixTS * 1000);
|
|
1189
|
+
return d.toLocaleDateString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
1190
|
+
},
|
|
1191
|
+
|
|
1192
|
+
renderYorum(y, isReply = false) {
|
|
1193
|
+
const avatar = y.profilFotografi
|
|
1194
|
+
? (y.profilFotografi.startsWith('http') ? y.profilFotografi : '/uploads/profiles/' + y.profilFotografi)
|
|
1195
|
+
: `https://sinavortagim.artilingo.com/system/default-avatar.png`;
|
|
1196
|
+
|
|
1197
|
+
return `
|
|
1198
|
+
<div class="sq-yorum-item ${isReply ? 'sq-yorum-reply' : ''}" id="yorum-${y.yorumId}">
|
|
1199
|
+
<a href="/user/profil/${window.escHtml(y.kullaniciAdi)}">
|
|
1200
|
+
${y.rol == 0 ? `
|
|
1201
|
+
<div class="is-mod-avatar" style="width: 35px; height: 35px; margin-right: 0.5rem;">
|
|
1202
|
+
<img src="${window.escHtml(avatar)}" class="sq-avatar" alt="Avatar" style="margin: 0; width:100%; height:100%;">
|
|
1203
|
+
</div>
|
|
1204
|
+
` : `
|
|
1205
|
+
<img src="${window.escHtml(avatar)}" class="sq-avatar" alt="Avatar">
|
|
1206
|
+
`}
|
|
1207
|
+
</a>
|
|
1208
|
+
<div class="sq-yorum-bubble">
|
|
1209
|
+
<div class="sq-yorum-header">
|
|
1210
|
+
<a href="/user/profil/${window.escHtml(y.kullaniciAdi)}" style="text-decoration: none; color: inherit;">
|
|
1211
|
+
<strong class="${y.rol == 0 ? 'is-mod-text' : ''}">${y.adSoyad ? window.escHtml(y.adSoyad) : '@' + window.escHtml(y.kullaniciAdi)}</strong>
|
|
1212
|
+
</a>
|
|
1213
|
+
${y.rol == 0 ? `<span class="badge-mod" style="font-size: 0.75rem; color: #a855f7; margin-left: 5px;"><i class="fa-solid fa-shield-halved"></i> Moderatör</span>` : ''}
|
|
1214
|
+
${!y.adSoyad ? '' : `<span>@${window.escHtml(y.kullaniciAdi)}</span>`}
|
|
1215
|
+
${y.rol != 0 ? `<span class="sq-yorum-role">${this.getRoleLabel(y.rol)}</span>` : ''}
|
|
1216
|
+
<span>${this.timeAgoJs(y.olusturulmaTarihi)}</span>
|
|
1217
|
+
</div>
|
|
1218
|
+
<div class="sq-yorum-metin">${this._renderMentions(y.yorumMetni).replace(/\n/g, '<br>')}</div>
|
|
1219
|
+
<div class="sq-yorum-footer">
|
|
1220
|
+
${!isReply ? `<button class="sq-yorum-action btn-reply" data-id="${y.yorumId}" data-user="@${window.escHtml(y.kullaniciAdi)}"><i class="fa-solid fa-reply"></i> Yanıtla</button>` : ''}
|
|
1221
|
+
${y.canEdit ? `<button class="sq-yorum-action edit btn-edit-yorum" data-id="${y.yorumId}" data-metin="${window.escHtml(y.yorumMetni)}"><i class="fa-solid fa-pen"></i> Düzenle</button>` : ''}
|
|
1222
|
+
${y.canDelete ? `<button class="sq-yorum-action delete btn-delete-yorum" data-id="${y.yorumId}"><i class="fa-solid fa-trash"></i> Sil</button>` : ''}
|
|
1223
|
+
${!y.canEdit ? `<button class="sq-yorum-action text-danger" onclick="reportContent(4, '${y.yorumId}')"><i class="fa-solid fa-flag"></i> Raporla</button>` : ''}
|
|
1224
|
+
</div>
|
|
1225
|
+
</div>
|
|
1226
|
+
</div>
|
|
1227
|
+
`;
|
|
1228
|
+
},
|
|
1229
|
+
|
|
1230
|
+
attachEvents() {
|
|
1231
|
+
const _this = this;
|
|
1232
|
+
document.querySelectorAll('.btn-reply').forEach(btn => {
|
|
1233
|
+
btn.addEventListener('click', function() {
|
|
1234
|
+
const replyTo = this.dataset.user; // '@kullaniciAdi'
|
|
1235
|
+
_this.aktifUstYorumId = this.dataset.id;
|
|
1236
|
+
document.getElementById('gcmReplyToUsername').textContent = replyTo;
|
|
1237
|
+
document.getElementById('gcmReplyStatus').classList.add('active');
|
|
1238
|
+
|
|
1239
|
+
// Editor'a @mention tag'i ekle
|
|
1240
|
+
const editor = document.getElementById('gcmYorumInput');
|
|
1241
|
+
editor.innerHTML = '';
|
|
1242
|
+
const tag = _this._createMentionTag(replyTo.replace('@', ''));
|
|
1243
|
+
editor.appendChild(tag);
|
|
1244
|
+
// tag sonrasına boflukluk ekle ve kursörü yerleştir
|
|
1245
|
+
const space = document.createTextNode('\u00a0');
|
|
1246
|
+
editor.appendChild(space);
|
|
1247
|
+
_this._placeCursorAfter(space);
|
|
1248
|
+
editor.dataset.placeholder = 'Yanıtını yaz...';
|
|
1249
|
+
editor.focus();
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
document.querySelectorAll('.sq-toggle-replies').forEach(btn => {
|
|
1254
|
+
btn.addEventListener('click', function() {
|
|
1255
|
+
const targetId = this.dataset.target;
|
|
1256
|
+
const container = document.getElementById(targetId);
|
|
1257
|
+
if (container.classList.contains('open')) {
|
|
1258
|
+
container.classList.remove('open');
|
|
1259
|
+
this.textContent = `Yanıtları gör (${container.children.length})`;
|
|
1260
|
+
} else {
|
|
1261
|
+
container.classList.add('open');
|
|
1262
|
+
this.textContent = `Yanıtları gizle`;
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
document.querySelectorAll('.btn-delete-yorum').forEach(btn => {
|
|
1268
|
+
btn.addEventListener('click', function() {
|
|
1269
|
+
window.showConfirm('Yorumu Sil', 'Bu yorumu silmek istediğine emin misin?', { confirmClass: 'btn-danger', confirmText: 'Sil' })
|
|
1270
|
+
.then(async (confirmed) => {
|
|
1271
|
+
if (!confirmed) return;
|
|
1272
|
+
|
|
1273
|
+
const id = this.dataset.id;
|
|
1274
|
+
const res = await window.apiPost('/api/switch-case/yorumGonderi', {
|
|
1275
|
+
action: 'delete',
|
|
1276
|
+
gonderiId: _this.aktifGonderiId,
|
|
1277
|
+
yorumId: id
|
|
1278
|
+
});
|
|
1279
|
+
if (res.status === 'success') {
|
|
1280
|
+
window.showToast('Yorum silindi.');
|
|
1281
|
+
_this.updateYorumCountUI(res.data?.yeniYorumSayisi);
|
|
1282
|
+
_this.loadYorumlar();
|
|
1283
|
+
} else {
|
|
1284
|
+
window.showToast(res.message, 'error');
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
});
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
document.querySelectorAll('.btn-edit-yorum').forEach(btn => {
|
|
1291
|
+
btn.addEventListener('click', function() {
|
|
1292
|
+
const yorumId = this.dataset.id;
|
|
1293
|
+
const mevcutMetin = this.dataset.metin;
|
|
1294
|
+
const bubble = this.closest('.sq-yorum-bubble');
|
|
1295
|
+
const metinEl = bubble.querySelector('.sq-yorum-metin');
|
|
1296
|
+
const footerEl = bubble.querySelector('.sq-yorum-footer');
|
|
1297
|
+
|
|
1298
|
+
// Zaten düzenleme modunda ise çık
|
|
1299
|
+
if (bubble.querySelector('.sq-edit-area')) return;
|
|
1300
|
+
|
|
1301
|
+
// Inline edit alanı oluştur
|
|
1302
|
+
const editArea = document.createElement('div');
|
|
1303
|
+
editArea.className = 'sq-edit-area';
|
|
1304
|
+
editArea.innerHTML = `
|
|
1305
|
+
<div class="cm-mention-dropdown sq-edit-mention-dropdown"></div>
|
|
1306
|
+
<div class="sq-edit-editor cm-editor sq-edit-textarea"
|
|
1307
|
+
contenteditable="true"
|
|
1308
|
+
data-placeholder="Yorumu düzenle..."
|
|
1309
|
+
role="textbox"
|
|
1310
|
+
aria-multiline="true"
|
|
1311
|
+
spellcheck="true"></div>
|
|
1312
|
+
<div class="sq-edit-actions">
|
|
1313
|
+
<button class="sq-yorum-action edit sq-edit-save"><i class="fa-solid fa-check"></i> Kaydet</button>
|
|
1314
|
+
<button class="sq-yorum-action sq-edit-cancel"><i class="fa-solid fa-xmark"></i> İptal</button>
|
|
1315
|
+
</div>
|
|
1316
|
+
`;
|
|
1317
|
+
|
|
1318
|
+
metinEl.style.display = 'none';
|
|
1319
|
+
footerEl.style.display = 'none';
|
|
1320
|
+
bubble.appendChild(editArea);
|
|
1321
|
+
|
|
1322
|
+
const editEditorEl = editArea.querySelector('.sq-edit-editor');
|
|
1323
|
+
const editDropdownEl = editArea.querySelector('.sq-edit-mention-dropdown');
|
|
1324
|
+
|
|
1325
|
+
// Mevcut metni editöre yükle
|
|
1326
|
+
editEditorEl.textContent = mevcutMetin;
|
|
1327
|
+
_this._attachMentionListeners(editEditorEl, editDropdownEl);
|
|
1328
|
+
editEditorEl.focus();
|
|
1329
|
+
// Kursörü sona taşı
|
|
1330
|
+
const selRange = document.createRange();
|
|
1331
|
+
selRange.selectNodeContents(editEditorEl);
|
|
1332
|
+
selRange.collapse(false);
|
|
1333
|
+
window.getSelection().removeAllRanges();
|
|
1334
|
+
window.getSelection().addRange(selRange);
|
|
1335
|
+
|
|
1336
|
+
// İptal
|
|
1337
|
+
editArea.querySelector('.sq-edit-cancel').addEventListener('click', () => {
|
|
1338
|
+
editArea.remove();
|
|
1339
|
+
metinEl.style.display = '';
|
|
1340
|
+
footerEl.style.display = '';
|
|
1341
|
+
// Ana editöre geri dön
|
|
1342
|
+
const mainEditor = document.getElementById('gcmYorumInput');
|
|
1343
|
+
const mainDd = document.getElementById('gcmMentionDropdown');
|
|
1344
|
+
if (mainEditor) { _this._currentEditor = mainEditor; _this._currentDropdown = mainDd; }
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
// Kaydet
|
|
1348
|
+
editArea.querySelector('.sq-edit-save').addEventListener('click', async () => {
|
|
1349
|
+
const yeniMetin = _this._getEditorText(editEditorEl).trim();
|
|
1350
|
+
if (!yeniMetin) return;
|
|
1351
|
+
|
|
1352
|
+
const saveBtn = editArea.querySelector('.sq-edit-save');
|
|
1353
|
+
saveBtn.disabled = true;
|
|
1354
|
+
saveBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
|
|
1355
|
+
|
|
1356
|
+
const res = await window.apiPost('/api/switch-case/yorumGonderi', {
|
|
1357
|
+
action: 'update',
|
|
1358
|
+
yorumId: yorumId,
|
|
1359
|
+
yorumMetni: yeniMetin
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
if (res.status === 'success') {
|
|
1363
|
+
// DOM'u anlık güncelle
|
|
1364
|
+
metinEl.innerHTML = _this._renderMentions(res.data.yeniMetin).replace(/\n/g, '<br>');
|
|
1365
|
+
btn.dataset.metin = res.data.yeniMetin;
|
|
1366
|
+
editArea.remove();
|
|
1367
|
+
metinEl.style.display = '';
|
|
1368
|
+
footerEl.style.display = '';
|
|
1369
|
+
// Ana editöre geri dön
|
|
1370
|
+
const mainEditor = document.getElementById('gcmYorumInput');
|
|
1371
|
+
const mainDd = document.getElementById('gcmMentionDropdown');
|
|
1372
|
+
if (mainEditor) { _this._currentEditor = mainEditor; _this._currentDropdown = mainDd; }
|
|
1373
|
+
window.showToast('Yorum güncellendi.');
|
|
1374
|
+
} else {
|
|
1375
|
+
window.showToast(res.message || 'Güncellenemedi.', 'error');
|
|
1376
|
+
saveBtn.disabled = false;
|
|
1377
|
+
saveBtn.innerHTML = '<i class="fa-solid fa-check"></i> Kaydet';
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
1382
|
+
},
|
|
1383
|
+
|
|
1384
|
+
updateYorumCountUI(count) {
|
|
1385
|
+
if (count !== undefined) {
|
|
1386
|
+
// Update on details page if available
|
|
1387
|
+
document.querySelectorAll('.sq-yorum-count, .sd-action-btn[data-soru-id="'+this.aktifGonderiId+'"] span').forEach(el => {
|
|
1388
|
+
el.textContent = count;
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
},
|
|
1392
|
+
|
|
1393
|
+
async sendYorum() {
|
|
1394
|
+
const editor = document.getElementById('gcmYorumInput');
|
|
1395
|
+
const btn = document.getElementById('cmBtnSendYorum');
|
|
1396
|
+
const metin = this._getEditorText(editor).trim();
|
|
1397
|
+
if (!metin) return;
|
|
1398
|
+
|
|
1399
|
+
btn.disabled = true;
|
|
1400
|
+
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
|
|
1401
|
+
|
|
1402
|
+
const res = await window.apiPost('/api/switch-case/yorumGonderi', {
|
|
1403
|
+
action: 'create',
|
|
1404
|
+
gonderiId: this.aktifGonderiId,
|
|
1405
|
+
yorumMetni: metin,
|
|
1406
|
+
ustYorumId: this.aktifUstYorumId || ''
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
btn.disabled = false;
|
|
1410
|
+
btn.innerHTML = '<i class="fa-solid fa-paper-plane"></i>';
|
|
1411
|
+
|
|
1412
|
+
if (res.status === 'success') {
|
|
1413
|
+
document.getElementById('gcmBtnCancelReply').click();
|
|
1414
|
+
editor.innerHTML = '';
|
|
1415
|
+
this.updateYorumCountUI(res.data?.yeniYorumSayisi);
|
|
1416
|
+
this.loadYorumlar();
|
|
1417
|
+
setTimeout(() => {
|
|
1418
|
+
const body = document.getElementById('gcmBody');
|
|
1419
|
+
body.scrollTop = body.scrollHeight;
|
|
1420
|
+
}, 300);
|
|
1421
|
+
} else {
|
|
1422
|
+
window.showToast(res.message || 'Gönderilemedi', 'error');
|
|
1423
|
+
}
|
|
1424
|
+
},
|
|
1425
|
+
|
|
1426
|
+
/* ---- Mention (Etiket) Sistemi ---- */
|
|
1427
|
+
|
|
1428
|
+
/** Editor'daki tüm metni (mention tag'leri dahil) düz string olarak al */
|
|
1429
|
+
_getEditorText(editor) {
|
|
1430
|
+
let text = '';
|
|
1431
|
+
editor.childNodes.forEach(node => {
|
|
1432
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1433
|
+
text += node.textContent;
|
|
1434
|
+
} else if (node.classList && node.classList.contains('cm-mention-tag')) {
|
|
1435
|
+
text += '@' + node.dataset.username;
|
|
1436
|
+
} else {
|
|
1437
|
+
text += node.textContent;
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
return text;
|
|
1441
|
+
},
|
|
1442
|
+
|
|
1443
|
+
/** Yorum metnindeki @kullaniciAdi'lerini mor tıklanabilir linke dönüştür */
|
|
1444
|
+
_renderMentions(text) {
|
|
1445
|
+
return window.escHtml(text)
|
|
1446
|
+
.replace(/@([a-zA-Z0-9_\.]{1,30})/g,
|
|
1447
|
+
'<a href="/user/profil/$1" class="cm-mention-link">@$1</a>');
|
|
1448
|
+
},
|
|
1449
|
+
|
|
1450
|
+
/** Mention tag span'i oluştur */
|
|
1451
|
+
_createMentionTag(username) {
|
|
1452
|
+
const span = document.createElement('span');
|
|
1453
|
+
span.className = 'cm-mention-tag';
|
|
1454
|
+
span.contentEditable = 'false';
|
|
1455
|
+
span.dataset.username = username;
|
|
1456
|
+
span.textContent = '@' + username;
|
|
1457
|
+
return span;
|
|
1458
|
+
},
|
|
1459
|
+
|
|
1460
|
+
/** Kursörü bir node sonrasına taşı */
|
|
1461
|
+
_placeCursorAfter(node) {
|
|
1462
|
+
const sel = window.getSelection();
|
|
1463
|
+
const range = document.createRange();
|
|
1464
|
+
range.setStartAfter(node);
|
|
1465
|
+
range.collapse(true);
|
|
1466
|
+
sel.removeAllRanges();
|
|
1467
|
+
sel.addRange(range);
|
|
1468
|
+
},
|
|
1469
|
+
|
|
1470
|
+
_attachMentionListeners(editorEl, dropdownEl) {
|
|
1471
|
+
editorEl.addEventListener('focus', () => {
|
|
1472
|
+
this._currentEditor = editorEl;
|
|
1473
|
+
this._currentDropdown = dropdownEl;
|
|
1474
|
+
});
|
|
1475
|
+
editorEl.addEventListener('input', () => this._onEditorInput());
|
|
1476
|
+
editorEl.addEventListener('keydown', (e) => this._onEditorKeydown(e));
|
|
1477
|
+
// İlk bağlamada hemen aktif yap
|
|
1478
|
+
this._currentEditor = editorEl;
|
|
1479
|
+
this._currentDropdown = dropdownEl;
|
|
1480
|
+
},
|
|
1481
|
+
|
|
1482
|
+
_closeMentionDropdown() {
|
|
1483
|
+
const dd = this._currentDropdown;
|
|
1484
|
+
if (dd) { dd.innerHTML = ''; dd.style.display = 'none'; }
|
|
1485
|
+
this._mentionQuery = null;
|
|
1486
|
+
},
|
|
1487
|
+
|
|
1488
|
+
_onEditorInput() {
|
|
1489
|
+
const sel = window.getSelection();
|
|
1490
|
+
if (!sel.rangeCount) return;
|
|
1491
|
+
|
|
1492
|
+
// Kursörün solundaki metni al
|
|
1493
|
+
const range = sel.getRangeAt(0);
|
|
1494
|
+
const textBefore = range.startContainer.textContent?.slice(0, range.startOffset) || '';
|
|
1495
|
+
|
|
1496
|
+
// Son @... patternını ara
|
|
1497
|
+
const match = textBefore.match(/@([a-zA-Z0-9_\.]*)$/);
|
|
1498
|
+
if (match && match[1].length >= 3) {
|
|
1499
|
+
const query = match[1];
|
|
1500
|
+
if (query !== this._mentionQuery) {
|
|
1501
|
+
this._mentionQuery = query;
|
|
1502
|
+
clearTimeout(this._mentionSearchTimer);
|
|
1503
|
+
this._mentionSearchTimer = setTimeout(() => this._fetchMentionSuggestions(query), 300);
|
|
1504
|
+
}
|
|
1505
|
+
} else {
|
|
1506
|
+
this._closeMentionDropdown();
|
|
1507
|
+
}
|
|
1508
|
+
},
|
|
1509
|
+
|
|
1510
|
+
_onEditorKeydown(e) {
|
|
1511
|
+
const dd = document.getElementById('gcmMentionDropdown');
|
|
1512
|
+
const items = dd?.querySelectorAll('.cm-mention-item');
|
|
1513
|
+
if (!items || items.length === 0 || dd.style.display === 'none') return;
|
|
1514
|
+
|
|
1515
|
+
const active = dd.querySelector('.cm-mention-item.active');
|
|
1516
|
+
if (e.key === 'ArrowDown') {
|
|
1517
|
+
e.preventDefault();
|
|
1518
|
+
const next = active ? active.nextElementSibling : items[0];
|
|
1519
|
+
active?.classList.remove('active');
|
|
1520
|
+
next?.classList.add('active');
|
|
1521
|
+
} else if (e.key === 'ArrowUp') {
|
|
1522
|
+
e.preventDefault();
|
|
1523
|
+
const prev = active ? active.previousElementSibling : items[items.length - 1];
|
|
1524
|
+
active?.classList.remove('active');
|
|
1525
|
+
prev?.classList.add('active');
|
|
1526
|
+
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
1527
|
+
if (active) {
|
|
1528
|
+
e.preventDefault();
|
|
1529
|
+
this._insertMention(active.dataset.username);
|
|
1530
|
+
}
|
|
1531
|
+
} else if (e.key === 'Escape') {
|
|
1532
|
+
this._closeMentionDropdown();
|
|
1533
|
+
}
|
|
1534
|
+
},
|
|
1535
|
+
|
|
1536
|
+
async _fetchMentionSuggestions(query) {
|
|
1537
|
+
try {
|
|
1538
|
+
const res = await window.apiPost('/api/switch-case/search', {
|
|
1539
|
+
action: 'search',
|
|
1540
|
+
keyword: query
|
|
1541
|
+
});
|
|
1542
|
+
if (res.status === 'success') {
|
|
1543
|
+
this._showMentionDropdown(res.data.slice(0, 5));
|
|
1544
|
+
}
|
|
1545
|
+
} catch (e) { /* sessiz hata */ }
|
|
1546
|
+
},
|
|
1547
|
+
|
|
1548
|
+
_showMentionDropdown(users) {
|
|
1549
|
+
const dd = this._currentDropdown;
|
|
1550
|
+
if (!users || users.length === 0) { this._closeMentionDropdown(); return; }
|
|
1551
|
+
|
|
1552
|
+
dd.innerHTML = users.map(u => {
|
|
1553
|
+
const avatar = u.profilFotografi
|
|
1554
|
+
? (u.profilFotografi.startsWith('http') ? u.profilFotografi : '/uploads/profiles/' + u.profilFotografi)
|
|
1555
|
+
: `https://sinavortagim.artilingo.com/system/default-avatar.png`;
|
|
1556
|
+
return `
|
|
1557
|
+
<div class="cm-mention-item" data-username="${window.escHtml(u.kullaniciAdi)}">
|
|
1558
|
+
<img src="${window.escHtml(avatar)}" alt="">
|
|
1559
|
+
<div class="cm-mention-info">
|
|
1560
|
+
<strong>${window.escHtml(u.adSoyad || u.kullaniciAdi)}</strong>
|
|
1561
|
+
<span>@${window.escHtml(u.kullaniciAdi)}</span>
|
|
1562
|
+
</div>
|
|
1563
|
+
</div>`;
|
|
1564
|
+
}).join('');
|
|
1565
|
+
dd.style.display = 'block';
|
|
1566
|
+
|
|
1567
|
+
dd.querySelectorAll('.cm-mention-item').forEach(item => {
|
|
1568
|
+
item.addEventListener('mousedown', (e) => {
|
|
1569
|
+
e.preventDefault();
|
|
1570
|
+
this._insertMention(item.dataset.username);
|
|
1571
|
+
});
|
|
1572
|
+
});
|
|
1573
|
+
},
|
|
1574
|
+
|
|
1575
|
+
_insertMention(username) {
|
|
1576
|
+
const editor = this._currentEditor;
|
|
1577
|
+
const sel = window.getSelection();
|
|
1578
|
+
if (!sel.rangeCount || !editor) return;
|
|
1579
|
+
|
|
1580
|
+
const range = sel.getRangeAt(0);
|
|
1581
|
+
const node = range.startContainer;
|
|
1582
|
+
const text = node.textContent || '';
|
|
1583
|
+
const offset = range.startOffset;
|
|
1584
|
+
|
|
1585
|
+
// @ ile başlayan kısmı sil
|
|
1586
|
+
const atIdx = text.lastIndexOf('@', offset - 1);
|
|
1587
|
+
if (atIdx !== -1) {
|
|
1588
|
+
node.textContent = text.slice(0, atIdx) + text.slice(offset);
|
|
1589
|
+
range.setStart(node, atIdx);
|
|
1590
|
+
range.collapse(true);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// Mention span ekle
|
|
1594
|
+
const tag = this._createMentionTag(username);
|
|
1595
|
+
range.insertNode(tag);
|
|
1596
|
+
|
|
1597
|
+
// Tag sonrasına boşluk ekle ve kursörü oraya taşı
|
|
1598
|
+
const space = document.createTextNode('\u00a0');
|
|
1599
|
+
tag.parentNode.insertBefore(space, tag.nextSibling);
|
|
1600
|
+
this._placeCursorAfter(space);
|
|
1601
|
+
|
|
1602
|
+
this._closeMentionDropdown();
|
|
1603
|
+
editor.focus();
|
|
1604
|
+
},
|
|
1605
|
+
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
window.toggleLikePost = function(btn, gonderiId) {
|
|
1609
|
+
const formData = new FormData();
|
|
1610
|
+
formData.append('action', 'toggleLike');
|
|
1611
|
+
formData.append('gonderiId', gonderiId);
|
|
1612
|
+
|
|
1613
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
|
|
1614
|
+
if (csrfToken) formData.append('csrf_token', csrfToken);
|
|
1615
|
+
|
|
1616
|
+
fetch('/api/switch-case/gonderi', {
|
|
1617
|
+
method: 'POST',
|
|
1618
|
+
body: formData
|
|
1619
|
+
})
|
|
1620
|
+
.then(res => res.json())
|
|
1621
|
+
.then(data => {
|
|
1622
|
+
if (data.status === 'success') {
|
|
1623
|
+
const icon = btn.querySelector('i.fa-heart');
|
|
1624
|
+
const span = btn.querySelector('span');
|
|
1625
|
+
|
|
1626
|
+
if (data.begendi) {
|
|
1627
|
+
btn.classList.add('liked');
|
|
1628
|
+
icon.classList.remove('fa-regular');
|
|
1629
|
+
icon.classList.add('fa-solid');
|
|
1630
|
+
icon.style.color = 'var(--danger-color, #ef4444)';
|
|
1631
|
+
} else {
|
|
1632
|
+
btn.classList.remove('liked');
|
|
1633
|
+
icon.classList.remove('fa-solid');
|
|
1634
|
+
icon.classList.add('fa-regular');
|
|
1635
|
+
icon.style.color = '';
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (span) {
|
|
1639
|
+
span.textContent = data.yeniSayisi;
|
|
1640
|
+
}
|
|
1641
|
+
} else {
|
|
1642
|
+
if (typeof window.showToast === 'function') {
|
|
1643
|
+
window.showToast(data.message || 'Hata oluştu', 'error');
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
})
|
|
1647
|
+
.catch(err => {
|
|
1648
|
+
if (typeof window.showToast === 'function') {
|
|
1649
|
+
window.showToast('Bağlantı hatası', 'error');
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
};
|
|
1653
|
+
|
|
1654
|
+
window.toggleSavePost = function(btn, gonderiId) {
|
|
1655
|
+
const formData = new FormData();
|
|
1656
|
+
formData.append('action', 'toggleSave');
|
|
1657
|
+
formData.append('gonderiId', gonderiId);
|
|
1658
|
+
|
|
1659
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
|
|
1660
|
+
if (csrfToken) formData.append('csrf_token', csrfToken);
|
|
1661
|
+
|
|
1662
|
+
fetch('/api/switch-case/gonderi', {
|
|
1663
|
+
method: 'POST',
|
|
1664
|
+
body: formData
|
|
1665
|
+
})
|
|
1666
|
+
.then(res => res.json())
|
|
1667
|
+
.then(data => {
|
|
1668
|
+
if (data.status === 'success') {
|
|
1669
|
+
const icon = btn.querySelector('i.fa-bookmark');
|
|
1670
|
+
const span = btn.querySelector('span') || btn.childNodes[btn.childNodes.length - 1];
|
|
1671
|
+
|
|
1672
|
+
if (data.kaydetti) {
|
|
1673
|
+
btn.classList.add('saved');
|
|
1674
|
+
icon.classList.remove('fa-regular');
|
|
1675
|
+
icon.classList.add('fa-solid');
|
|
1676
|
+
icon.style.color = 'var(--brand-primary, #3b82f6)';
|
|
1677
|
+
} else {
|
|
1678
|
+
btn.classList.remove('saved');
|
|
1679
|
+
icon.classList.remove('fa-solid');
|
|
1680
|
+
icon.classList.add('fa-regular');
|
|
1681
|
+
icon.style.color = '';
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// Eğer butonda span veya metin node'u varsa sayıyı güncelle
|
|
1685
|
+
if (data.yeniSayisi !== undefined) {
|
|
1686
|
+
if (btn.querySelector('span')) {
|
|
1687
|
+
btn.querySelector('span').textContent = data.yeniSayisi;
|
|
1688
|
+
} else {
|
|
1689
|
+
btn.innerHTML = `<i class="${data.kaydetti ? 'fa-solid' : 'fa-regular'} fa-bookmark" ${data.kaydetti ? 'style="color: var(--brand-primary, #3b82f6);"' : ''}></i> ${data.yeniSayisi}`;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
} else {
|
|
1693
|
+
if (typeof window.showToast === 'function') {
|
|
1694
|
+
window.showToast(data.message || 'Hata oluştu', 'error');
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
})
|
|
1698
|
+
.catch(err => {
|
|
1699
|
+
if (typeof window.showToast === 'function') {
|
|
1700
|
+
window.showToast('Bağlantı hatası', 'error');
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
/* --- Global Mention Manager --- */
|
|
1707
|
+
class GlobalMentionManager {
|
|
1708
|
+
constructor(editorElement, dropdownElement) {
|
|
1709
|
+
this.editor = typeof editorElement === 'string' ? document.querySelector(editorElement) : editorElement;
|
|
1710
|
+
this.dropdown = typeof dropdownElement === 'string' ? document.querySelector(dropdownElement) : dropdownElement;
|
|
1711
|
+
|
|
1712
|
+
this.mentionQuery = null;
|
|
1713
|
+
this.searchTimer = null;
|
|
1714
|
+
this.activeRange = null;
|
|
1715
|
+
|
|
1716
|
+
if (this.editor && this.dropdown) {
|
|
1717
|
+
this.initListeners();
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
initListeners() {
|
|
1722
|
+
this.editor.addEventListener('input', (e) => this.handleInput(e));
|
|
1723
|
+
this.editor.addEventListener('keydown', (e) => this.handleKeyDown(e));
|
|
1724
|
+
document.addEventListener('click', (e) => {
|
|
1725
|
+
if (!this.editor.contains(e.target) && !this.dropdown.contains(e.target)) {
|
|
1726
|
+
this.closeDropdown();
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
handleInput(e) {
|
|
1732
|
+
const selection = window.getSelection();
|
|
1733
|
+
if (!selection.rangeCount) return;
|
|
1734
|
+
const range = selection.getRangeAt(0);
|
|
1735
|
+
|
|
1736
|
+
// Sadece metin düğümlerinde işlem yap
|
|
1737
|
+
if (range.startContainer.nodeType !== Node.TEXT_NODE) {
|
|
1738
|
+
this.closeDropdown();
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
const text = range.startContainer.textContent;
|
|
1743
|
+
const offset = range.startOffset;
|
|
1744
|
+
|
|
1745
|
+
// İmleçten geriye doğru @ işaretini ara (boşluk veya string başı olmalı)
|
|
1746
|
+
const textBeforeCursor = text.slice(0, offset);
|
|
1747
|
+
const match = textBeforeCursor.match(/(?:^|\s)@(\w*)$/);
|
|
1748
|
+
|
|
1749
|
+
if (match) {
|
|
1750
|
+
const query = match[1];
|
|
1751
|
+
this.activeRange = range.cloneRange(); // Konumu kaydet
|
|
1752
|
+
|
|
1753
|
+
if (query !== this.mentionQuery) {
|
|
1754
|
+
this.mentionQuery = query;
|
|
1755
|
+
clearTimeout(this.searchTimer);
|
|
1756
|
+
this.searchTimer = setTimeout(() => this.fetchSuggestions(query), 300);
|
|
1757
|
+
}
|
|
1758
|
+
} else {
|
|
1759
|
+
this.closeDropdown();
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
handleKeyDown(e) {
|
|
1764
|
+
if (this.dropdown.style.display !== 'block') return;
|
|
1765
|
+
|
|
1766
|
+
const items = this.dropdown.querySelectorAll('.cm-mention-item');
|
|
1767
|
+
if (items.length === 0) return;
|
|
1768
|
+
|
|
1769
|
+
const active = this.dropdown.querySelector('.cm-mention-item.active');
|
|
1770
|
+
let index = Array.from(items).indexOf(active);
|
|
1771
|
+
|
|
1772
|
+
if (e.key === 'ArrowDown') {
|
|
1773
|
+
e.preventDefault();
|
|
1774
|
+
if (active) active.classList.remove('active');
|
|
1775
|
+
index = (index + 1) % items.length;
|
|
1776
|
+
items[index].classList.add('active');
|
|
1777
|
+
items[index].scrollIntoView({ block: 'nearest' });
|
|
1778
|
+
} else if (e.key === 'ArrowUp') {
|
|
1779
|
+
e.preventDefault();
|
|
1780
|
+
if (active) active.classList.remove('active');
|
|
1781
|
+
index = (index - 1 + items.length) % items.length;
|
|
1782
|
+
items[index].classList.add('active');
|
|
1783
|
+
items[index].scrollIntoView({ block: 'nearest' });
|
|
1784
|
+
} else if (e.key === 'Enter') {
|
|
1785
|
+
e.preventDefault();
|
|
1786
|
+
if (active) {
|
|
1787
|
+
this.insertMention(active.dataset.username);
|
|
1788
|
+
}
|
|
1789
|
+
} else if (e.key === 'Escape') {
|
|
1790
|
+
this.closeDropdown();
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
async fetchSuggestions(query) {
|
|
1795
|
+
if (!query && query !== '') return;
|
|
1796
|
+
|
|
1797
|
+
try {
|
|
1798
|
+
const res = await window.apiPost('/api/switch-case/search', {
|
|
1799
|
+
action: 'search',
|
|
1800
|
+
keyword: query
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
if (res.status === 'success' && res.data.length > 0) {
|
|
1804
|
+
this.showDropdown(res.data.slice(0, 5));
|
|
1805
|
+
} else {
|
|
1806
|
+
this.closeDropdown();
|
|
1807
|
+
}
|
|
1808
|
+
} catch (err) {
|
|
1809
|
+
console.error('Mention arama hatası', err);
|
|
1810
|
+
this.closeDropdown();
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
showDropdown(users) {
|
|
1815
|
+
if (!users || users.length === 0) {
|
|
1816
|
+
this.closeDropdown();
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
let html = '';
|
|
1821
|
+
users.forEach((u, idx) => {
|
|
1822
|
+
const avatar = u.profilFotografi || 'https://sinavortagim.artilingo.com/system/default-avatar.png';
|
|
1823
|
+
html += `
|
|
1824
|
+
<div class="cm-mention-item ${idx === 0 ? 'active' : ''}" data-username="${window.escHtml(u.kullaniciAdi)}">
|
|
1825
|
+
<img src="${window.escHtml(avatar)}" alt="Avatar">
|
|
1826
|
+
<div class="cm-mention-info">
|
|
1827
|
+
<strong>${window.escHtml(u.adSoyad)}</strong>
|
|
1828
|
+
<span>@${window.escHtml(u.kullaniciAdi)}</span>
|
|
1829
|
+
</div>
|
|
1830
|
+
</div>
|
|
1831
|
+
`;
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
this.dropdown.innerHTML = html;
|
|
1835
|
+
this.dropdown.style.display = 'block';
|
|
1836
|
+
|
|
1837
|
+
this.dropdown.querySelectorAll('.cm-mention-item').forEach(item => {
|
|
1838
|
+
item.addEventListener('mouseenter', () => {
|
|
1839
|
+
this.dropdown.querySelector('.active')?.classList.remove('active');
|
|
1840
|
+
item.classList.add('active');
|
|
1841
|
+
});
|
|
1842
|
+
item.addEventListener('click', () => {
|
|
1843
|
+
this.insertMention(item.dataset.username);
|
|
1844
|
+
});
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
closeDropdown() {
|
|
1849
|
+
this.dropdown.style.display = 'none';
|
|
1850
|
+
this.mentionQuery = null;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
insertMention(username) {
|
|
1854
|
+
if (!this.activeRange) return;
|
|
1855
|
+
|
|
1856
|
+
const selection = window.getSelection();
|
|
1857
|
+
selection.removeAllRanges();
|
|
1858
|
+
selection.addRange(this.activeRange);
|
|
1859
|
+
|
|
1860
|
+
const node = this.activeRange.startContainer;
|
|
1861
|
+
const text = node.textContent || '';
|
|
1862
|
+
const offset = this.activeRange.startOffset;
|
|
1863
|
+
|
|
1864
|
+
// @ işaretini bul ve sil
|
|
1865
|
+
const atIdx = text.lastIndexOf('@', offset - 1);
|
|
1866
|
+
if (atIdx !== -1) {
|
|
1867
|
+
node.textContent = text.slice(0, atIdx) + text.slice(offset);
|
|
1868
|
+
this.activeRange.setStart(node, atIdx);
|
|
1869
|
+
this.activeRange.collapse(true);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// Tag oluştur
|
|
1873
|
+
const span = document.createElement('span');
|
|
1874
|
+
span.className = 'cm-mention-tag';
|
|
1875
|
+
span.contentEditable = 'false';
|
|
1876
|
+
span.dataset.username = username;
|
|
1877
|
+
span.textContent = '@' + username;
|
|
1878
|
+
|
|
1879
|
+
this.activeRange.insertNode(span);
|
|
1880
|
+
|
|
1881
|
+
// Boşluk ekle ve imleci sona al
|
|
1882
|
+
const space = document.createTextNode('\u00a0');
|
|
1883
|
+
span.parentNode.insertBefore(space, span.nextSibling);
|
|
1884
|
+
|
|
1885
|
+
selection.removeAllRanges();
|
|
1886
|
+
const newRange = document.createRange();
|
|
1887
|
+
newRange.setStart(space, 1);
|
|
1888
|
+
newRange.collapse(true);
|
|
1889
|
+
selection.addRange(newRange);
|
|
1890
|
+
|
|
1891
|
+
this.closeDropdown();
|
|
1892
|
+
this.editor.focus();
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// Gönderim öncesi tüm metni düz yazıya çevirme
|
|
1896
|
+
getPlainText() {
|
|
1897
|
+
return this.editor.innerText.trim();
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
window.GlobalMentionManager = GlobalMentionManager;
|