diff --git a/backend/src/main/java/com/rit/portal/controller/CommunityQuestionController.java b/backend/src/main/java/com/rit/portal/controller/CommunityQuestionController.java index 3167ef1..7951bef 100644 --- a/backend/src/main/java/com/rit/portal/controller/CommunityQuestionController.java +++ b/backend/src/main/java/com/rit/portal/controller/CommunityQuestionController.java @@ -5,6 +5,10 @@ import com.rit.portal.entity.CommunityQuestion; import com.rit.portal.repository.CommunityAnswerRepository; import com.rit.portal.repository.CommunityQuestionRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; @@ -26,19 +30,33 @@ public class CommunityQuestionController { private final RestTemplate restTemplate = new RestTemplate(); private static final String TELEGRAM_BOT_URL = "http://localhost:8082/send_question"; + /** Legacy – returns all questions (kept for backwards compatibility) */ @GetMapping public List getAllQuestions() { return questionRepository.findAll(); } + /** + * Paginated – returns a Spring Page of questions ordered newest-first. + * Usage: GET /api/questions/paged?page=0&size=5 + * Response includes: content[], totalElements, totalPages, number (current page), last (boolean) + */ + @GetMapping("/paged") + public Page getQuestionsPaged( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "5") int size) { + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + return questionRepository.findAll(pageable); + } + @PostMapping public CommunityQuestion createQuestion(@RequestBody CommunityQuestion question) { if (question.getUpvotes() == null) question.setUpvotes(0); if (question.getIsAnswered() == null) question.setIsAnswered(false); question.setCreatedAt(LocalDateTime.now()); - + CommunityQuestion saved = questionRepository.save(question); - + // Notify Telegram bot in a background thread to keep it robust and non-blocking new Thread(() -> { try { @@ -47,7 +65,7 @@ public class CommunityQuestionController { payload.put("title", saved.getTitle()); payload.put("body", saved.getBody()); payload.put("author", saved.getAuthor()); - + restTemplate.postForEntity(TELEGRAM_BOT_URL, payload, String.class); } catch (Exception e) { System.err.println("Failed to notify Telegram Bot intermediary: " + e.getMessage()); @@ -64,12 +82,12 @@ public class CommunityQuestionController { if (answer.getUpvotes() == null) answer.setUpvotes(0); if (answer.getIsAccepted() == null) answer.setIsAccepted(false); answer.setCreatedAt(LocalDateTime.now()); - + CommunityAnswer savedAnswer = answerRepository.save(answer); - + question.setIsAnswered(true); questionRepository.save(question); - + return ResponseEntity.ok(savedAnswer); }).orElse(ResponseEntity.notFound().build()); } diff --git a/src/pages/Community/Community.tsx b/src/pages/Community/Community.tsx index 9b63b83..e8b1eea 100644 --- a/src/pages/Community/Community.tsx +++ b/src/pages/Community/Community.tsx @@ -64,6 +64,8 @@ const getRelativeTime = (dateString: string) => { }; export default function Community() { + const PAGE_SIZE = 5; + const [activeTab, setActiveTab] = useState('qa'); const [confessionText, setConfessionText] = useState(''); const [questionText, setQuestionText] = useState(''); @@ -72,8 +74,13 @@ export default function Community() { const [searchQuery, setSearchQuery] = useState(''); const [likedIds, setLikedIds] = useState>(new Set()); const [expandedQuestionIds, setExpandedQuestionIds] = useState>(new Set()); - const [questions, setQuestions] = useState(QUESTIONS_DATA); - const [visibleCount, setVisibleCount] = useState(5); + // Server-side pagination + const [currentPage, setCurrentPage] = useState(0); + const [totalPages, setTotalPages] = useState(1); + const [questions, setQuestions] = useState([]); + const [loading, setLoading] = useState(false); + // Fallback static questions for when backend is unavailable + const [staticQuestions] = useState(QUESTIONS_DATA); const toggleAnswers = (id: string) => { setExpandedQuestionIds((prev) => { @@ -87,30 +94,37 @@ export default function Community() { }); }; - const fetchQuestions = async () => { + const fetchQuestions = async (page = 0) => { + setLoading(true); try { - const response = await fetch('http://localhost:8080/api/questions'); + const response = await fetch(`http://localhost:8080/api/questions/paged?page=${page}&size=${PAGE_SIZE}`); if (response.ok) { const data = await response.json(); - console.log("API DATA FETCHED:", data); - if (data) { - // Merge database questions with static mock data, matching IDs to prevent duplicates - const backendIds = new Set(data.map((q: any) => q.id.toString())); - const uniqueMocks = QUESTIONS_DATA.filter(q => !backendIds.has(q.id.toString())); - const merged = [...[...data].reverse(), ...uniqueMocks]; - console.log("MERGED QUESTIONS LIST:", merged); - setQuestions(merged); - } + // Spring Page response: { content: [], totalPages, totalElements, number } + setQuestions(data.content || []); + setTotalPages(data.totalPages || 1); + setCurrentPage(data.number ?? page); } else { console.error("Failed to fetch questions from backend: HTTP status", response.status); + // Fallback: slice static questions + const start = page * PAGE_SIZE; + setQuestions(staticQuestions.slice(start, start + PAGE_SIZE)); + setTotalPages(Math.ceil(staticQuestions.length / PAGE_SIZE)); + setCurrentPage(page); } } catch (error) { console.error('Backend not available. Falling back to local static questions data.', error); + const start = page * PAGE_SIZE; + setQuestions(staticQuestions.slice(start, start + PAGE_SIZE)); + setTotalPages(Math.ceil(staticQuestions.length / PAGE_SIZE)); + setCurrentPage(page); + } finally { + setLoading(false); } }; useEffect(() => { - fetchQuestions(); + fetchQuestions(0); }, []); const getAnswersCount = (q: any) => { @@ -124,6 +138,7 @@ export default function Community() { return votesVal; }; + // Client-side filter applied on top of current page (for search within page) const filteredQuestions = questions.filter((q) => { const titleVal = q.title || ""; const tagsVal = q.tags || []; @@ -131,8 +146,12 @@ export default function Community() { tagsVal.some((t: string) => t.toLowerCase().includes(searchQuery.toLowerCase())); }); - const visibleQuestions = filteredQuestions.slice(0, visibleCount); - const hasMore = visibleCount < filteredQuestions.length; + const handlePageChange = (newPage: number) => { + setExpandedQuestionIds(new Set()); // collapse any open answers + fetchQuestions(newPage); + // Scroll back to the top of the questions list smoothly + document.getElementById('qa-questions-list')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }; const handleConfess = () => { if (!confessionText.trim()) return; @@ -306,16 +325,33 @@ export default function Community() { type="text" placeholder="Search questions..." value={searchQuery} - onChange={(e) => { setSearchQuery(e.target.value); setVisibleCount(5); }} + onChange={(e) => setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-slate-200 bg-white text-[13px] text-slate-800 placeholder-slate-400 focus:outline-none focus:border-slate-400 transition-colors" style={{ fontFamily: 'Inter, sans-serif' }} /> {/* Questions */} - - {visibleQuestions.map((q, idx) => { - const isFeatured = idx === 0 && searchQuery === ''; +
+ {loading ? ( +
+ {[...Array(PAGE_SIZE)].map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+ ))} +
+ ) : ( + + {filteredQuestions.map((q, idx) => { + const isFeatured = idx === 0 && searchQuery === '' && currentPage === 0; return ( ); - })} - + })} + + )} - {/* Load More / Show Less */} - {filteredQuestions.length > 5 && ( -
- {hasMore ? ( - <> -

- Showing {visibleCount} of {filteredQuestions.length} questions -

+ {/* Pagination Controls */} + {totalPages > 1 && ( +
+ handlePageChange(currentPage - 1)} + disabled={currentPage === 0 || loading} + className={`flex items-center gap-1.5 px-4 py-2 rounded-xl border text-[12px] font-semibold transition-all duration-200 cursor-pointer ${ + currentPage === 0 || loading + ? 'border-slate-100 bg-slate-50 text-slate-300 cursor-not-allowed' + : 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50 hover:border-slate-300 shadow-sm' + }`} + style={{ fontFamily: 'Poppins, sans-serif' }} + > + + Prev + + +
+ {Array.from({ length: totalPages }).map((_, i) => ( setVisibleCount((c) => c + 5)} - className="flex items-center gap-2 px-5 py-2.5 rounded-xl border border-slate-200 bg-white text-slate-700 text-[13px] font-semibold hover:bg-slate-50 hover:border-slate-300 transition-all duration-200 cursor-pointer shadow-sm" + key={i} + whileHover={{ scale: 1.1 }} + whileTap={{ scale: 0.9 }} + onClick={() => handlePageChange(i)} + disabled={loading} + className={`w-7 h-7 rounded-lg text-[11px] font-bold transition-all duration-200 cursor-pointer border ${ + i === currentPage + ? 'bg-slate-900 text-white border-slate-900' + : 'bg-white text-slate-500 border-slate-200 hover:border-slate-300 hover:bg-slate-50' + }`} style={{ fontFamily: 'Poppins, sans-serif' }} > - - Load More Questions + {i + 1} - - ) : ( - setVisibleCount(5)} - className="flex items-center gap-2 px-5 py-2.5 rounded-xl border border-slate-200 bg-white text-slate-500 text-[13px] font-medium hover:bg-slate-50 hover:border-slate-300 transition-all duration-200 cursor-pointer" - style={{ fontFamily: 'Poppins, sans-serif' }} - > - - Show Less - - )} + ))} +
+ + handlePageChange(currentPage + 1)} + disabled={currentPage >= totalPages - 1 || loading} + className={`flex items-center gap-1.5 px-4 py-2 rounded-xl border text-[12px] font-semibold transition-all duration-200 cursor-pointer ${ + currentPage >= totalPages - 1 || loading + ? 'border-slate-100 bg-slate-50 text-slate-300 cursor-not-allowed' + : 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50 hover:border-slate-300 shadow-sm' + }`} + style={{ fontFamily: 'Poppins, sans-serif' }} + > + Next + +
)} +
{/* Sidebar */}