@@ -10,8 +10,9 @@ const inputId = `comment-input-${postId}`;
1010
1111 <div class =" comment-form-container" data-comment-form style =" display: none;" >
1212 <label for ={ inputId } class =" sr-only" >Yorumunuz</label >
13- <textarea id ={ inputId } name =" comment" data-comment-input placeholder =" Düşüncelerinizi paylaşın..." ></textarea >
14- <button class =" comment-btn" data-comment-submit type =" button" >Yorumu Gönder</button >
13+ <textarea id ={ inputId } name =" comment" data-comment-input placeholder =" Düşüncelerinizi paylaşın..." minlength =" 5" maxlength =" 300" ></textarea >
14+ <p class =" char-counter" data-char-counter >0 / 300</p >
15+ <button class =" comment-btn" data-comment-submit type =" button" disabled >Yorumu Gönder</button >
1516
1617 <p class =" success-message" data-success-message style =" display: none;" >
1718 ✓ Yorumunuz başarıyla gönderildi! Yönetici onayından sonra sayfada görünecektir.
@@ -26,6 +27,10 @@ const inputId = `comment-input-${postId}`;
2627 import { onAuthStateChanged } from "firebase/auth";
2728 import { collection, addDoc, query, where, orderBy, onSnapshot, serverTimestamp } from "firebase/firestore";
2829
30+ const MIN_LENGTH = 5;
31+ const MAX_LENGTH = 300;
32+ const COOLDOWN_MS = 60_000;
33+
2934 const safeDate = (ts) => {
3035 if (!ts || typeof ts.toDate !== 'function') return 'az önce';
3136 return ts.toDate().toLocaleString('tr-TR', {
@@ -34,9 +39,8 @@ const inputId = `comment-input-${postId}`;
3439 });
3540 };
3641
37- // HEMERA'NIN PERFORMANS TAVSİYESİ: Dinleyicileri hafızada tutacağımız değişkenler
38- let unsubscribeAuth = null;
39- let unsubscribeSnapshot = null;
42+ const unsubscribers = new Set();
43+ const intervals = new Set();
4044
4145 const initCommentWidget = (wrapper) => {
4246 if (!wrapper || wrapper.dataset.bound === '1') return;
@@ -47,85 +51,153 @@ const inputId = `comment-input-${postId}`;
4751 const formContainer = wrapper.querySelector('[data-comment-form]');
4852 const loginPrompt = wrapper.querySelector('[data-login-prompt]');
4953 const commentInput = wrapper.querySelector('[data-comment-input]');
54+ const charCounter = wrapper.querySelector('[data-char-counter]');
5055 const submitBtn = wrapper.querySelector('[data-comment-submit]');
5156 const successMsg = wrapper.querySelector('[data-success-message]');
5257
58+ if (!postId || !commentInput || !submitBtn || !charCounter) return;
59+
60+ const cooldownKey = 'commentCooldownGlobal';
61+
5362 let currentUser = null;
63+ let cooldownUntil = 0;
64+ let cooldownInterval = null;
65+
66+ const stopCooldown = () => {
67+ if (!cooldownInterval) return;
68+ clearInterval(cooldownInterval);
69+ intervals.delete(cooldownInterval);
70+ cooldownInterval = null;
71+ };
72+
73+ const updateSubmitState = () => {
74+ const value = commentInput.value || '';
75+ const length = value.length;
76+ const trimmedLength = value.trim().length;
77+ const now = Date.now();
78+ const inCooldown = cooldownUntil > now;
79+
80+ charCounter.textContent = `${length} / ${MAX_LENGTH}`;
81+ charCounter.classList.toggle('danger', length > MAX_LENGTH);
82+
83+ if (!currentUser) {
84+ submitBtn.disabled = true;
85+ submitBtn.textContent = 'Yorumu Gönder';
86+ return;
87+ }
88+
89+ if (inCooldown) {
90+ const remainingSec = Math.ceil((cooldownUntil - now) / 1000);
91+ submitBtn.disabled = true;
92+ submitBtn.textContent = `Yeni yorum için ${remainingSec}s...`;
93+ return;
94+ }
95+
96+ submitBtn.textContent = 'Yorumu Gönder';
97+ submitBtn.disabled = trimmedLength < MIN_LENGTH || length > MAX_LENGTH;
98+ };
99+
100+ const startCooldown = (targetTime) => {
101+ cooldownUntil = targetTime;
102+ localStorage.setItem(cooldownKey, String(cooldownUntil));
103+ stopCooldown();
104+ updateSubmitState();
105+
106+ cooldownInterval = setInterval(() => {
107+ if (Date.now() >= cooldownUntil) {
108+ localStorage.removeItem(cooldownKey);
109+ cooldownUntil = 0;
110+ stopCooldown();
111+ }
112+ updateSubmitState();
113+ }, 250);
114+
115+ intervals.add(cooldownInterval);
116+ };
117+
118+ const savedCooldown = Number(localStorage.getItem(cooldownKey) || 0);
119+ if (savedCooldown > Date.now()) {
120+ startCooldown(savedCooldown);
121+ } else {
122+ localStorage.removeItem(cooldownKey);
123+ }
54124
55- // Dinleyiciyi değişkene atıyoruz ki sonra kapatabilelim
56- unsubscribeAuth = onAuthStateChanged(auth, (user) => {
125+ const unsubscribeAuth = onAuthStateChanged(auth, (user) => {
57126 if (!formContainer || !loginPrompt) return;
58127 if (user) {
59128 currentUser = user;
60- formContainer.style.display = "flex";
129+ formContainer.style.display = "flex";
61130 loginPrompt.style.display = "none";
62131 } else {
63132 currentUser = null;
64133 formContainer.style.display = "none";
65134 loginPrompt.style.display = "block";
66135 }
136+ updateSubmitState();
67137 });
68138
69- if (postId) {
70- const q = query(
71- collection(db, "comments"),
72- where("postId", "==", postId),
73- where("status", "==", "approved"),
74- orderBy("createdAt", "asc")
75- );
76-
77- // Dinleyiciyi değişkene atıyoruz ki sonra kapatabilelim
78- unsubscribeSnapshot = onSnapshot(q, (snapshot) => {
79- if (!commentsList) return;
80- commentsList.innerHTML = "";
81-
82- if (snapshot.empty) {
83- commentsList.innerHTML = '<p class="empty-state">Henüz onaylanmış yorum yok. İlk yorumu sen yap!</p>';
84- return;
85- }
139+ unsubscribers.add(unsubscribeAuth);
86140
87- snapshot.forEach((docSnap) => {
88- const data = docSnap.data();
89- const item = document.createElement('article');
90- item.className = 'comment-item';
141+ const q = query(
142+ collection(db, "comments"),
143+ where("postId", "==", postId),
144+ where("status", "==", "approved"),
145+ orderBy("createdAt", "asc")
146+ );
91147
92- const top = document.createElement('div');
93- top.className = 'comment-top';
148+ const unsubscribeSnapshot = onSnapshot(q, (snapshot) => {
149+ if (!commentsList) return;
150+ commentsList.innerHTML = "";
94151
95- const name = document.createElement('strong');
96- name.className = 'comment-author';
97- name.textContent = data.authorName || 'Kullanıcı';
152+ if (snapshot.empty) {
153+ commentsList.innerHTML = '<p class="empty-state">Henüz onaylanmış yorum yok. İlk yorumu sen yap!</p>';
154+ return;
155+ }
98156
99- const time = document.createElement('span');
100- time.className = 'comment-time';
101- time.textContent = safeDate(data.createdAt);
157+ snapshot.forEach((docSnap) => {
158+ const data = docSnap.data();
159+ const item = document.createElement('article');
160+ item.className = 'comment-item';
102161
103- top.appendChild(name );
104- top.appendChild(time) ;
162+ const top = document.createElement('div' );
163+ top.className = 'comment- top' ;
105164
106- const text = document.createElement('p ');
107- text .className = 'comment-text ';
108- text .textContent = data.text || '';
165+ const name = document.createElement('strong ');
166+ name .className = 'comment-author ';
167+ name .textContent = data.authorName || 'Kullanıcı ';
109168
110- item.appendChild(top);
111- item.appendChild(text);
169+ const time = document.createElement('span');
170+ time.className = 'comment-time';
171+ time.textContent = safeDate(data.createdAt);
112172
113- commentsList.appendChild(item);
114- });
115- }, (error) => {
116- console.error("Yorumlar çekilirken hata:", error);
173+ top.appendChild(name);
174+ top.appendChild(time);
175+
176+ const text = document.createElement('p');
177+ text.className = 'comment-text';
178+ text.textContent = data.text || '';
179+
180+ item.appendChild(top);
181+ item.appendChild(text);
182+
183+ commentsList.appendChild(item);
117184 });
118- }
185+ }, (error) => {
186+ console.error("Yorumlar çekilirken hata:", error);
187+ });
188+
189+ unsubscribers.add(unsubscribeSnapshot);
119190
120- submitBtn?.addEventListener("click", async () => {
121- if (!commentInput || !postId) return;
191+ commentInput.addEventListener('input', updateSubmitState);
122192
193+ submitBtn.addEventListener("click", async () => {
123194 const text = commentInput.value.trim();
124- if (!text || !currentUser) return;
195+ if (!currentUser || text.length < MIN_LENGTH || text.length > MAX_LENGTH) return;
196+ if (Date.now() < cooldownUntil) return;
125197
126198 try {
127199 submitBtn.textContent = "Gönderiliyor...";
128- submitBtn.setAttribute(" disabled", " true") ;
200+ submitBtn.disabled = true;
129201 successMsg.style.display = "none";
130202
131203 await addDoc(collection(db, "comments"), {
@@ -138,29 +210,36 @@ const inputId = `comment-input-${postId}`;
138210 });
139211
140212 commentInput.value = "";
141-
142- // HEMERA'NIN UX TAVSİYESİ: Çirkin alert yerine şık yazı
213+ updateSubmitState();
214+
143215 successMsg.style.display = "block";
144- setTimeout(() => { successMsg.style.display = "none"; }, 5000); // 5 saniye sonra gizle
145-
216+ setTimeout(() => { successMsg.style.display = "none"; }, 5000);
217+
218+ startCooldown(Date.now() + COOLDOWN_MS);
146219 } catch (error) {
147220 console.error("Yorum eklenirken hata oluştu:", error);
148- } finally {
149- submitBtn.textContent = "Yorumu Gönder";
150- submitBtn.removeAttribute("disabled");
221+ updateSubmitState();
151222 }
152223 });
224+
225+ updateSubmitState();
153226 };
154227
155- // Astro View Transitions için: Sayfa her değiştiğinde sistemi yeniden başlat
156- document.addEventListener('astro:page-load', () => {
228+ const bootCommentWidgets = () => {
157229 document.querySelectorAll('[data-comment-widget]').forEach(initCommentWidget);
158- });
230+ };
231+
232+ bootCommentWidgets();
233+ document.addEventListener('astro:page-load', bootCommentWidgets);
159234
160- // HEMERA'NIN PERFORMANS TAVSİYESİ: Sayfa değişirken dinleyicileri (listener) temizle
161235 document.addEventListener('astro:before-swap', () => {
162- if (unsubscribeAuth) unsubscribeAuth();
163- if (unsubscribeSnapshot) unsubscribeSnapshot();
236+ unsubscribers.forEach((unsub) => {
237+ try { unsub(); } catch {}
238+ });
239+ unsubscribers.clear();
240+
241+ intervals.forEach((id) => clearInterval(id));
242+ intervals.clear();
164243 });
165244</script >
166245
@@ -177,8 +256,10 @@ const inputId = `comment-input-${postId}`;
177256 .comment-form-container { display: flex; flex-direction: column; gap: 0.55rem; margin-top: 0.7rem; }
178257 textarea[data-comment-input] { width: 100%; min-height: 110px; border: 1px solid var(--border); border-radius: 0.8rem; background: var(--card-bg); color: var(--text); padding: 0.8rem; resize: vertical; font: inherit; }
179258 textarea[data-comment-input]:focus { outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 1px; }
259+ .char-counter { margin: -0.1rem 0 0; color: var(--secondary); font-size: 0.78rem; text-align: right; }
260+ .char-counter.danger { color: #ef4444; }
180261 .comment-btn { align-self: flex-end; border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border)); background: var(--accent); color: white; border-radius: 999px; padding: 0.5rem 0.95rem; cursor: pointer; font-weight: 600; }
181- .comment-btn:disabled { opacity: 0.7; cursor: wait ; }
262+ .comment-btn:disabled { opacity: 0.7; cursor: not-allowed ; }
182263 .login-prompt, .empty-state { color: var(--secondary); font-size: 0.9rem; }
183264 .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
184265
0 commit comments