diff --git a/.gitignore b/.gitignore index 2791342..8e4bb48 100644 --- a/.gitignore +++ b/.gitignore @@ -23,10 +23,3 @@ dist-ssr *.sln *.sw? *.db - -# Environments -.env -.env.local -.env.* -.env.development -.env.production 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 a97b8fb..3167ef1 100644 --- a/backend/src/main/java/com/rit/portal/controller/CommunityQuestionController.java +++ b/backend/src/main/java/com/rit/portal/controller/CommunityQuestionController.java @@ -31,19 +31,6 @@ public class CommunityQuestionController { return questionRepository.findAll(); } - @GetMapping("/paged") - public org.springframework.data.domain.Page getPagedQuestions( - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "5") int size) { - return questionRepository.findAll( - org.springframework.data.domain.PageRequest.of( - page, - size, - org.springframework.data.domain.Sort.by("createdAt").descending() - ) - ); - } - @PostMapping public CommunityQuestion createQuestion(@RequestBody CommunityQuestion question) { if (question.getUpvotes() == null) question.setUpvotes(0); diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 36a1399..f68d37a 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,5 +1,5 @@ # ─── DATABASE CONNECTION CONFIGURATION ─── -spring.datasource.url=jdbc:postgresql://localhost:5432/rit_freshers_hub?sslmode=disable +spring.datasource.url=jdbc:postgresql://localhost:5433/rit_freshers_hub?sslmode=disable spring.datasource.username=postgres spring.datasource.password=${DB_PASSWORD:Anbukathir@#$2006} diff --git a/src/constants/index.ts b/src/constants/index.ts index 3b21d5b..8f6216b 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -861,29 +861,7 @@ export const QUESTIONS_DATA: Question[] = [ ]; // ─── Confessions ───────────────────────────────────────────────────────────── -export const CONFESSIONS_DATA: Confession[] = [ - { - id: '1', - content: 'I was so nervous on my first day that I went to the wrong department. A senior helped me out and became my best friend. RIT has such a warm community! 🧡', - createdAt: '2025-07-19T11:00:00Z', - reactions: 87, - category: 'Wholesome', - }, - { - id: '2', - content: 'The library WiFi is actually faster than my home connection. No complaints from me 😂', - createdAt: '2025-07-18T15:30:00Z', - reactions: 134, - category: 'Funny', - }, - { - id: '3', - content: 'I have a crush on someone in my class but I\'m too shy to talk. We have been in the same group project for 2 months now 😅', - createdAt: '2025-07-17T09:00:00Z', - reactions: 212, - category: 'Crush', - }, -]; +export const CONFESSIONS_DATA: Confession[] = []; // ─── Toolkit Items ─────────────────────────────────────────────────────────── export const TOOLKIT_ITEMS: ToolkitItem[] = [ diff --git a/src/index.css b/src/index.css index 74c3655..c5a32cf 100644 --- a/src/index.css +++ b/src/index.css @@ -84,7 +84,6 @@ h1, h2, h3, h4, h5, h6 { :focus-visible { outline: 2px solid #F97316; outline-offset: 2px; - border-radius: 4px; } /* Animations */ @@ -294,3 +293,16 @@ h1, h2, h3, h4, h5, h6 { align-self: flex-start; border: 1px solid #E5E7EB; } + +/* Verified card style input focus state */ +.verified-focus-input { + transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease; + background-color: #FFFFFF; + border: 1.5px solid #E5E7EB !important; +} + +.verified-focus-input:focus { + outline: none !important; + border-color: #F97316 !important; + box-shadow: none !important; +} diff --git a/src/pages/Community/Community.tsx b/src/pages/Community/Community.tsx index 0d712ba..b5ad697 100644 --- a/src/pages/Community/Community.tsx +++ b/src/pages/Community/Community.tsx @@ -7,11 +7,9 @@ import { import SectionTitle from '@/components/SectionTitle/SectionTitle'; import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer'; import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer'; -import { QUESTIONS_DATA, CONFESSIONS_DATA } from '@/constants'; +import { CONFESSIONS_DATA } from '@/constants'; -type Tab = 'qa' | 'confession'; - -const TRENDING_TAGS = ['hostel', 'academics', 'clubs', 'campus', 'canteen', 'sports', 'placement', 'library']; +const TRENDING_TAGS = ['hostel', 'academics', 'clubs', 'campus', 'canteen', 'sports', 'placement', 'library', 'cat exam', 'semester', 'labs', 'exams']; const AVATARS: Record = { 'Priya S.': 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=120&h=120&q=80', @@ -25,7 +23,7 @@ const getAvatar = (author: string) => { const getRelativeTime = (dateString: string) => { if (!dateString) return 'just now'; - + // Normalize microsecond timestamps (e.g. 2026-07-22T13:28:18.174367) to standard millisecond precision let normalized = dateString; const dotIndex = dateString.indexOf('.'); @@ -66,21 +64,17 @@ 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(''); const [authorName, setAuthorName] = useState(''); - const [confessionPosted, setConfessionPosted] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [likedIds, setLikedIds] = useState>(new Set()); const [expandedQuestionIds, setExpandedQuestionIds] = useState>(new Set()); - // Server-side pagination + const [expandedBodyIds, setExpandedBodyIds] = useState>(new Set()); + const [sortFilter, setSortFilter] = useState<'recent' | 'liked' | 'answered' | 'unanswered'>('recent'); + // Client-side pagination state 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) => { @@ -94,37 +88,35 @@ export default function Community() { }); }; - const fetchQuestions = async (page = 0) => { + const fetchQuestions = async () => { setLoading(true); try { - const response = await fetch(`http://localhost:8080/api/questions/paged?page=${page}&size=${PAGE_SIZE}`); + const response = await fetch(`http://localhost:8080/api/questions`); if (response.ok) { const data = await response.json(); - // Spring Page response: { content: [], totalPages, totalElements, number } - setQuestions(data.content || []); - setTotalPages(data.totalPages || 1); - setCurrentPage(data.number ?? page); + // Backend returns oldest-first, reverse to show newest first + setQuestions(Array.isArray(data) ? [...data].reverse() : []); } 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); + setQuestions([]); } - } 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); + } catch { + // Backend unavailable — show empty state + setQuestions([]); } finally { setLoading(false); } }; useEffect(() => { - fetchQuestions(0); + const savedLikes = localStorage.getItem('qa-liked-ids'); + if (savedLikes) { + try { + setLikedIds(new Set(JSON.parse(savedLikes))); + } catch (e) { + console.error('Failed to parse liked IDs from localStorage', e); + } + } + fetchQuestions(); }, []); const getAnswersCount = (q: any) => { @@ -138,55 +130,109 @@ export default function Community() { return votesVal; }; - // Client-side filter applied on top of current page (for search within page) - const filteredQuestions = questions.filter((q) => { + // Client-side filter and sorting derived state + let processedQuestions = questions.filter((q) => { const titleVal = q.title || ""; const tagsVal = q.tags || []; return titleVal.toLowerCase().includes(searchQuery.toLowerCase()) || tagsVal.some((t: string) => t.toLowerCase().includes(searchQuery.toLowerCase())); }); + if (sortFilter === 'recent') { + processedQuestions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + } else if (sortFilter === 'liked') { + processedQuestions.sort((a, b) => getVotesCount(b) - getVotesCount(a)); + } else if (sortFilter === 'answered') { + processedQuestions.sort((a, b) => getAnswersCount(b) - getAnswersCount(a)); + } else if (sortFilter === 'unanswered') { + processedQuestions = processedQuestions.filter(q => !q.isAnswered && getAnswersCount(q) === 0); + } + + const filteredQuestions = processedQuestions; + const totalPages = Math.max(1, Math.ceil(filteredQuestions.length / PAGE_SIZE)); + const paginatedQuestions = filteredQuestions.slice(currentPage * PAGE_SIZE, (currentPage + 1) * PAGE_SIZE); + + // Reset page when search or filter changes + useEffect(() => { + setCurrentPage(0); + }, [searchQuery, sortFilter]); + const handlePageChange = (newPage: number) => { setExpandedQuestionIds(new Set()); // collapse any open answers - fetchQuestions(newPage); + setExpandedBodyIds(new Set()); // collapse bodies too + setCurrentPage(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; - setConfessionPosted(true); - setConfessionText(''); - setTimeout(() => setConfessionPosted(false), 3000); - }; - - const toggleLike = async (id: string) => { + const handleLike = async (id: string) => { + let isLikedNow = false; setLikedIds((prev) => { const next = new Set(prev); - if (next.has(id)) next.delete(id); else next.add(id); + if (next.has(id)) { + next.delete(id); + isLikedNow = false; + } else { + next.add(id); + isLikedNow = true; + } + localStorage.setItem('qa-liked-ids', JSON.stringify(Array.from(next))); return next; }); + // Optimistically update count (+1 if liked, -1 if unliked) + setQuestions((prev) => + prev.map((q) => { + if (q.id.toString() === id) { + const votesVal = typeof q.upvotes === 'number' ? q.upvotes : (typeof q.votes === 'number' ? q.votes : 0); + return { ...q, upvotes: isLikedNow ? votesVal + 1 : Math.max(0, votesVal - 1) }; + } + return q; + }) + ); + try { - await fetch(`http://localhost:8080/api/questions/${id}/upvote`, { - method: 'POST' - }); - fetchQuestions(); + if (isLikedNow) { + await fetch(`http://localhost:8080/api/questions/${id}/upvote`, { + method: 'POST' + }); + fetchQuestions(); + } } catch (error) { console.log('Backend not available for upvote sync.', error); } }; + const handleCardClick = (id: string) => { + const idStr = id.toString(); + setExpandedQuestionIds((prev) => { + const next = new Set(prev); + if (next.has(idStr)) next.delete(idStr); else next.add(idStr); + return next; + }); + setExpandedBodyIds((prev) => { + const next = new Set(prev); + if (next.has(idStr)) next.delete(idStr); else next.add(idStr); + return next; + }); + }; + const handlePostQuestion = async () => { if (!questionText.trim()) return; - + const displayAuthor = authorName.trim() || 'Anonymous'; - + + // Auto-detect hashtags based on question text content + const detectedTags = TRENDING_TAGS.filter(tag => + questionText.toLowerCase().includes(tag.toLowerCase()) + ); + const finalTags = detectedTags.length > 0 ? detectedTags : ['fresher', 'general']; + const newQuestion = { title: questionText.split('\n')[0].substring(0, 100) || "Q&A Question", body: questionText, author: displayAuthor, - tags: ['fresher', 'general'], + tags: finalTags, upvotes: 0, isAnswered: false, createdAt: new Date().toISOString(), @@ -232,7 +278,7 @@ export default function Community() { {/* Header */}
-

+

RIT Community

@@ -242,54 +288,16 @@ export default function Community() {

- {/* Tabs */} -
- {[ - { id: 'qa' as Tab, label: 'Freshers Q&A', icon: MessageCircle }, - { id: 'confession' as Tab, label: 'Confessions', icon: Heart }, - ].map((tab) => ( - - ))} -
- - - {/* Q&A Tab */} - {activeTab === 'qa' && ( - -
-
- {/* Ask Question Box */} -
-

Ask a Question

+
+
{/* Ask Question Box */} +
+

Ask a Question