feat: persist club and center likes in postgresql database
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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 {
|
||||
|
||||
83
src/services/clubService.ts
Normal file
83
src/services/clubService.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user