feat: persist club and center likes in postgresql database

This commit is contained in:
Amudieshwar
2026-08-19 08:31:12 +05:30
parent f8ca5a3368
commit 5f3b3f5973
7 changed files with 315 additions and 5 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useMemo } from 'react';
import { useState, useMemo, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import {
@@ -13,6 +13,13 @@ import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/An
import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer';
import { CLUBS_DATA } from '@/constants';
import type { Club } from '@/types';
import { useAuth } from '@/context/AuthContext';
import {
getClubUserIdentifier,
fetchClubLikes,
fetchUserLikedClubs,
toggleClubLikeInDb,
} from '@/services/clubService';
const CLUB_CATEGORY_COLORS: Record<string, string> = {
Technical: '#3B82F6',
@@ -119,6 +126,9 @@ const QUIZ_QUESTIONS_ALL = [
];
export default function Events() {
const { user } = useAuth();
const userIdentifier = useMemo(() => getClubUserIdentifier(user?.email), [user?.email]);
const [selectedClub, setSelectedClub] = useState<Club | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string>('All');
@@ -140,7 +150,7 @@ export default function Events() {
return { totalCount: CLUBS_DATA.length, centersCount: centers, clubsCount: clubs };
}, []);
// ─── Interactive Like System ─────────────────────────────────────────────
// ─── Database-Backed Interactive Like System ─────────────────────────────
const [likedClubs, setLikedClubs] = useState<Set<string>>(() => {
try {
localStorage.removeItem('rit_freshers_liked_clubs');
@@ -159,10 +169,45 @@ export default function Events() {
return initialMap;
});
const toggleLike = (clubId: string) => {
// Sync likes with database backend on mount and when user identity changes
useEffect(() => {
let isMounted = true;
async function syncWithDatabase() {
// Fetch total like counts for all clubs from database
const dbLikesMap = await fetchClubLikes();
if (isMounted && Object.keys(dbLikesMap).length > 0) {
setLikesMap((prev) => {
const merged = { ...prev };
Object.entries(dbLikesMap).forEach(([id, count]) => {
merged[id] = count;
});
return merged;
});
}
// Fetch user's liked clubs from database
const userLikedList = await fetchUserLikedClubs(userIdentifier);
if (isMounted && userLikedList.length > 0) {
setLikedClubs(new Set(userLikedList));
try {
localStorage.setItem('rit_freshers_liked_clubs_v3', JSON.stringify(userLikedList));
} catch {}
}
}
syncWithDatabase();
return () => {
isMounted = false;
};
}, [userIdentifier]);
const toggleLike = async (clubId: string) => {
const isCurrentlyLiked = likedClubs.has(clubId);
const next = new Set(likedClubs);
// Optimistic UI update
if (isCurrentlyLiked) {
next.delete(clubId);
setLikesMap((l) => ({ ...l, [clubId]: Math.max(0, (l[clubId] || 1) - 1) }));
@@ -175,8 +220,24 @@ export default function Events() {
try {
localStorage.setItem('rit_freshers_liked_clubs_v3', JSON.stringify(Array.from(next)));
} catch {}
// Persist like directly in PostgreSQL database backend
const dbResult = await toggleClubLikeInDb(clubId, userIdentifier);
if (dbResult) {
setLikesMap((l) => ({ ...l, [clubId]: dbResult.count }));
if (dbResult.liked) {
setLikedClubs((prev) => new Set([...prev, clubId]));
} else {
setLikedClubs((prev) => {
const updated = new Set(prev);
updated.delete(clubId);
return updated;
});
}
}
};
// ─── Quiz Matcher State ──────────────────────────────────────────────────
const [isQuizOpen, setIsQuizOpen] = useState(false);
const [matcherTarget, setMatcherTarget] = useState<'All' | 'Club' | 'Center'>('All');

View File

@@ -74,8 +74,12 @@ export default function StudentDashboard() {
setLoginError(null);
try {
const session = await loginWithIms(regNumberInput.trim(), passwordInput.trim());
setImsSession(session);
const res = await loginWithIms(regNumberInput.trim(), passwordInput.trim());
if (res.success && res.token && res.student) {
setImsSession({ token: res.token, student: res.student });
} else {
setLoginError(res.message || 'Invalid credentials or Register Number not found.');
}
} catch (err: any) {
setLoginError(err.message || 'Invalid credentials or Register Number not found.');
} finally {

View File

@@ -0,0 +1,83 @@
import { getBackendUrl } from '@/lib/utils';
const DEVICE_ID_KEY = 'rit_device_identifier';
/**
* Gets a persistent user identifier for likes.
* If user is authenticated, uses email. Otherwise uses a persistent UUID in localStorage.
*/
export function getClubUserIdentifier(userEmail?: string | null): string {
if (userEmail && userEmail.trim()) {
return userEmail.trim().toLowerCase();
}
try {
let deviceId = localStorage.getItem(DEVICE_ID_KEY);
if (!deviceId) {
deviceId = 'guest_' + Math.random().toString(36).substring(2, 11) + Date.now().toString(36);
localStorage.setItem(DEVICE_ID_KEY, deviceId);
}
return deviceId;
} catch {
return 'guest_fallback_user';
}
}
/**
* Fetch total counts of likes for all clubs & centers from database
*/
export async function fetchClubLikes(): Promise<Record<string, number>> {
try {
const res = await fetch(getBackendUrl('/api/clubs/likes'));
if (!res.ok) {
throw new Error(`Failed to fetch likes: ${res.status}`);
}
const data = await res.json();
return data || {};
} catch (err) {
console.warn('Unable to fetch club likes from database backend:', err);
return {};
}
}
/**
* Fetch user's liked club IDs from database
*/
export async function fetchUserLikedClubs(userIdentifier: string): Promise<string[]> {
try {
const res = await fetch(
getBackendUrl(`/api/clubs/user-likes?userIdentifier=${encodeURIComponent(userIdentifier)}`)
);
if (!res.ok) {
throw new Error(`Failed to fetch user likes: ${res.status}`);
}
const data = await res.json();
return Array.isArray(data) ? data : [];
} catch (err) {
console.warn('Unable to fetch user liked clubs from database backend:', err);
return [];
}
}
/**
* Toggle like status for a club/center in database
*/
export async function toggleClubLikeInDb(
clubId: string,
userIdentifier: string
): Promise<{ liked: boolean; count: number } | null> {
try {
const res = await fetch(getBackendUrl(`/api/clubs/${encodeURIComponent(clubId)}/like`), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userIdentifier }),
});
if (!res.ok) {
throw new Error(`Toggle like failed: ${res.status}`);
}
return await res.json();
} catch (err) {
console.warn(`Failed to persist like for club ${clubId} in database:`, err);
return null;
}
}