Skip to content

Commit 12ffb80

Browse files
committed
feat: Yorum spam koruması
1 parent f25dd1f commit 12ffb80

1 file changed

Lines changed: 148 additions & 67 deletions

File tree

src/components/CommentSection.astro

Lines changed: 148 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)