@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.
Files changed (90) hide show
  1. package/LICENSE +660 -660
  2. package/bin/artiframe.js +0 -0
  3. package/bin/artiframe.php +0 -0
  4. package/core-stubs/app/ApiAuthControl.php +76 -0
  5. package/core-stubs/app/ApiControl.php +81 -35
  6. package/core-stubs/app/AuthControl.php +39 -0
  7. package/core-stubs/app/Database.php +36 -35
  8. package/core-stubs/app/DotEnv.php +19 -1
  9. package/core-stubs/app/ViewControl.php +47 -1
  10. package/core-stubs/bin/SQLMethod.php +261 -0
  11. package/core-stubs/bin/SystemMethod.php +115 -74
  12. package/core-stubs/bin/ViewMethod.php +19 -5
  13. package/core-stubs/docs/de.html +4325 -4347
  14. package/core-stubs/docs/en.html +4325 -4347
  15. package/core-stubs/docs/es.html +4325 -4347
  16. package/core-stubs/docs/fr.html +4325 -4347
  17. package/core-stubs/docs/tr.html +117 -46
  18. package/core-stubs/public/403.php +52 -0
  19. package/core-stubs/public/assets/css/components/header.css +174 -0
  20. package/core-stubs/public/assets/css/components/mobilenav.css +198 -0
  21. package/core-stubs/public/assets/css/components/sidebar.css +184 -0
  22. package/core-stubs/public/assets/css/root/app.css +975 -38
  23. package/core-stubs/public/assets/css/root/root.css +64 -0
  24. package/core-stubs/public/assets/js/components/header.js +380 -0
  25. package/core-stubs/public/assets/js/components/mobilenav.js +97 -0
  26. package/core-stubs/public/assets/js/components/theme-modal.js +74 -0
  27. package/core-stubs/public/assets/js/root/app.js +1870 -0
  28. package/core-stubs/public/includes/footer.php +4 -5
  29. package/core-stubs/public/includes/head-auth.php +65 -0
  30. package/core-stubs/public/includes/head.php +56 -17
  31. package/core-stubs/public/includes/header.php +135 -1
  32. package/core-stubs/public/includes/mobilenav.php +75 -3
  33. package/core-stubs/public/includes/sidebar.php +186 -1
  34. package/core-stubs/public/includes/theme-modal.php +1 -0
  35. package/db-studio/debug.log +3 -0
  36. package/db-studio/index.html +1755 -0
  37. package/db-studio/launcher.html +102 -0
  38. package/db-studio/launcher.js +180 -0
  39. package/db-studio/logo.svg +22 -0
  40. package/db-studio/main.js +568 -0
  41. package/db-studio/package-lock.json +1034 -0
  42. package/db-studio/package.json +16 -0
  43. package/db-studio/preload.js +29 -0
  44. package/devops-studio/api-inspector.js +222 -0
  45. package/devops-studio/index.html +1892 -0
  46. package/devops-studio/logo.svg +22 -0
  47. package/devops-studio/main.js +67 -0
  48. package/devops-studio/make-wizard.js +635 -0
  49. package/devops-studio/package-lock.json +871 -0
  50. package/devops-studio/package.json +12 -0
  51. package/package.json +5 -2
  52. package/scripts/postinstall.js +44 -8
  53. package/server-studio/.json-404-guard +1 -0
  54. package/server-studio/index.html +283 -0
  55. package/server-studio/logo.svg +22 -0
  56. package/server-studio/main.js +133 -0
  57. package/server-studio/package-lock.json +1179 -0
  58. package/server-studio/package.json +15 -0
  59. package/server-studio/router.php +99 -0
  60. package/src/App.php +29 -0
  61. package/src/Commands/AddCommand.php +26 -9
  62. package/src/Commands/DbExportCommand.php +131 -0
  63. package/src/Commands/DbStartCommand.php +46 -0
  64. package/src/Commands/DevopsCommand.php +35 -0
  65. package/src/Commands/GoCommand.php +48 -48
  66. package/src/Commands/ListCommand.php +68 -68
  67. package/src/Commands/MakeApiCommand.php +6 -1
  68. package/src/Commands/MakeViewCommand.php +25 -2
  69. package/src/Commands/NewProjectCommand.php +16 -11
  70. package/src/Commands/OptimizeCommand.php +61 -0
  71. package/src/Commands/RemoveCommand.php +36 -7
  72. package/src/Commands/ServeCommand.php +46 -91
  73. package/src/Commands/ShowCommand.php +24 -24
  74. package/src/Commands/UpgradeCommand.php +39 -10
  75. package/stubs/make/api-switch-case.stub +1 -1
  76. package/stubs/make/auth-api-standart.stub +20 -0
  77. package/stubs/make/auth-api-switch-case.stub +38 -0
  78. package/stubs/make/auth-view.stub +26 -0
  79. package/stubs/make/class.stub +10 -2
  80. package/stubs/make/view.stub +3 -0
  81. package/stubs/service/image.stub +334 -334
  82. package/stubs/service/iyzico.stub +637 -637
  83. package/stubs/service/jwt.stub +312 -312
  84. package/stubs/service/phpmailer.stub +143 -143
  85. package/stubs/service/pusher.stub +232 -232
  86. package/stubs/service/qrcode.stub +169 -169
  87. package/stubs/service/redis.stub +361 -361
  88. package/stubs/service/s3.stub +377 -377
  89. package/stubs/service/stripe.stub +526 -526
  90. 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, '&lt;')
19
+ .replace(/>/g, '&gt;')
20
+ .replace(/"/g, '&quot;');
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;