Implement moderated Q&A board: questions only display after being answered
This commit is contained in:
@@ -28,7 +28,7 @@ public class CommunityQuestionController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<CommunityQuestion> getAllQuestions() {
|
public List<CommunityQuestion> getAllQuestions() {
|
||||||
return questionRepository.findAll();
|
return questionRepository.findByIsAnsweredTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package com.rit.portal.repository;
|
|||||||
import com.rit.portal.entity.CommunityQuestion;
|
import com.rit.portal.entity.CommunityQuestion;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface CommunityQuestionRepository extends JpaRepository<CommunityQuestion, Integer> {
|
public interface CommunityQuestionRepository extends JpaRepository<CommunityQuestion, Integer> {
|
||||||
|
List<CommunityQuestion> findByIsAnsweredTrue();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ export default function Community() {
|
|||||||
const [currentPage, setCurrentPage] = useState(0);
|
const [currentPage, setCurrentPage] = useState(0);
|
||||||
const [questions, setQuestions] = useState<any[]>([]);
|
const [questions, setQuestions] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [showSuccessBanner, setShowSuccessBanner] = useState(false);
|
||||||
|
const [postError, setPostError] = useState<string | null>(null);
|
||||||
|
|
||||||
const toggleAnswers = (id: string) => {
|
const toggleAnswers = (id: string) => {
|
||||||
setExpandedQuestionIds((prev) => {
|
setExpandedQuestionIds((prev) => {
|
||||||
@@ -229,48 +231,33 @@ export default function Community() {
|
|||||||
);
|
);
|
||||||
const finalTags = detectedTags.length > 0 ? detectedTags : ['fresher', 'general'];
|
const finalTags = detectedTags.length > 0 ? detectedTags : ['fresher', 'general'];
|
||||||
|
|
||||||
const newQuestion = {
|
setPostError(null);
|
||||||
title: questionText.split('\n')[0].substring(0, 100) || "Q&A Question",
|
setShowSuccessBanner(false);
|
||||||
body: questionText,
|
|
||||||
author: displayAuthor,
|
|
||||||
tags: finalTags,
|
|
||||||
upvotes: 0,
|
|
||||||
isAnswered: false,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
answers: []
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(getBackendUrl('/api/questions'), {
|
const response = await fetch(getBackendUrl('/api/questions'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
title: newQuestion.title,
|
title: questionText.split('\n')[0].substring(0, 100) || "Q&A Question",
|
||||||
body: newQuestion.body,
|
body: questionText,
|
||||||
author: newQuestion.author,
|
author: displayAuthor,
|
||||||
tags: newQuestion.tags
|
tags: finalTags
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const saved = await response.json();
|
setShowSuccessBanner(true);
|
||||||
setQuestions(prev => {
|
|
||||||
// Prepend saved question and filter out duplicate placeholders
|
|
||||||
const filtered = prev.filter(q => q.id.toString() !== saved.id.toString());
|
|
||||||
return [saved, ...filtered];
|
|
||||||
});
|
|
||||||
setQuestionText('');
|
setQuestionText('');
|
||||||
setAuthorName('');
|
setAuthorName('');
|
||||||
|
// Hide success banner after 6 seconds
|
||||||
|
setTimeout(() => setShowSuccessBanner(false), 6000);
|
||||||
} else {
|
} else {
|
||||||
setQuestions(prev => [{ ...newQuestion, id: String(Date.now()) }, ...prev]);
|
setPostError("Failed to submit your question. Please try again later.");
|
||||||
setQuestionText('');
|
|
||||||
setAuthorName('');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving question:", error);
|
console.error("Error saving question:", error);
|
||||||
setQuestions(prev => [{ ...newQuestion, id: String(Date.now()) }, ...prev]);
|
setPostError("Unable to connect to the server. Please check your connection.");
|
||||||
setQuestionText('');
|
|
||||||
setAuthorName('');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -293,6 +280,29 @@ export default function Community() {
|
|||||||
<div className="lg:col-span-2"> {/* Ask Question Box */}
|
<div className="lg:col-span-2"> {/* Ask Question Box */}
|
||||||
<div className="bg-white border border-[#E5E7EB] rounded-2xl p-6 mb-8" style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}>
|
<div className="bg-white border border-[#E5E7EB] rounded-2xl p-6 mb-8" style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}>
|
||||||
<h3 className="text-base font-semibold text-[#1E293B] mb-3 tracking-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>Ask a Question</h3>
|
<h3 className="text-base font-semibold text-[#1E293B] mb-3 tracking-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>Ask a Question</h3>
|
||||||
|
|
||||||
|
{showSuccessBanner && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="mb-4 p-4.5 bg-emerald-50 border border-emerald-200 text-emerald-800 rounded-xl text-xs font-medium leading-relaxed"
|
||||||
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
|
>
|
||||||
|
🎉 <strong>Question submitted!</strong> Your question has been forwarded to senior volunteers. Once they review and answer it, it will be published here on the community Q&A board.
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{postError && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="mb-4 p-4 bg-rose-50 border border-rose-200 text-rose-850 rounded-xl text-xs font-semibold"
|
||||||
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
|
>
|
||||||
|
⚠️ {postError}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
value={questionText}
|
value={questionText}
|
||||||
onChange={(e) => setQuestionText(e.target.value)}
|
onChange={(e) => setQuestionText(e.target.value)}
|
||||||
@@ -310,8 +320,8 @@ export default function Community() {
|
|||||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[11px] text-slate-400" style={{ fontFamily: 'Inter, sans-serif' }}>
|
<span className="text-[11px] text-slate-400 max-w-[70%] leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||||
Your question will be visible to all students
|
Your question will be sent to the senior helpers and published once answered.
|
||||||
</span>
|
</span>
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.01 }}
|
whileHover={{ scale: 1.01 }}
|
||||||
|
|||||||
Reference in New Issue
Block a user