diff --git a/components/repeto/FilterSection.tsx b/components/repeto/FilterSection.tsx index 9ffe2a5..c56162c 100644 --- a/components/repeto/FilterSection.tsx +++ b/components/repeto/FilterSection.tsx @@ -1,4 +1,6 @@ -import { useState } from 'react'; +"use client"; + +import React, { useState, useEffect, useRef } from 'react'; import { ChevronDown, ChevronUp, Filter, X } from 'lucide-react'; import filtersData from '@/data/filters.json'; @@ -7,6 +9,12 @@ interface FilterOption { options: string[]; } +// Special filter types interface +interface RangeFilter { + min: number; + max: number; +} + interface FilterSectionProps { onFilterSubmit?: (selectedFilters: Record) => void; onClearFilters?: () => void; @@ -17,6 +25,14 @@ export default function FilterSection({ onFilterSubmit, onClearFilters }: Filter const [filters] = useState(filtersData); const [selectedOptions, setSelectedOptions] = useState>>({}); const [openDropdowns, setOpenDropdowns] = useState>({}); + + // Get current year for year range filter + const currentYear = new Date().getFullYear(); + // Year range filter state + const [yearRangeFilter, setYearRangeFilter] = useState({ + min: 2020, // Default minimum year + max: currentYear + 5 // Default maximum year (current year + 5 for future years) + }); const toggleDropdown = (filterTitle: string) => { @@ -47,17 +63,36 @@ export default function FilterSection({ onFilterSubmit, onClearFilters }: Filter const handleSubmit = () => { const formattedFilters: Record = {}; + + // Add all selected filters except for Year of Submission (which will be handled by range) Object.entries(selectedOptions).forEach(([title, optionsSet]) => { - if (optionsSet.size > 0) { + if (optionsSet.size > 0 && title !== "Year of Submission") { formattedFilters[title] = Array.from(optionsSet); } }); + + // Generate all years between min and max for the Year of Submission filter + const yearsInRange: string[] = []; + for (let year = yearRangeFilter.min; year <= yearRangeFilter.max; year++) { + yearsInRange.push(year.toString()); + } + + // Add the year range to the filters + if (yearsInRange.length > 0) { + formattedFilters["Year of Submission"] = yearsInRange; + } + onFilterSubmit?.(Object.keys(formattedFilters).length > 0 ? formattedFilters : {}); setIsOpen(false); }; const clearFilters = () => { setSelectedOptions({}); + // Reset year range filter to defaults + setYearRangeFilter({ + min: 2020, + max: currentYear + 5 + }); setIsOpen(false); onClearFilters?.(); }; @@ -91,6 +126,8 @@ export default function FilterSection({ onFilterSubmit, onClearFilters }: Filter isSelected={isSelected} toggleOption={toggleOption} /> + ) : filter.title === "Year of Submission" ? ( + ) : (
{filter.options.map((option) => ( @@ -133,6 +170,523 @@ export default function FilterSection({ onFilterSubmit, onClearFilters }: Filter ); // Domain filter options component with search functionality + // Year Range Filter Component + const YearRangeFilterOptions = () => { + // Create a range of all possible years for the year picker + const minPossibleYear = 2000; + const maxPossibleYear = currentYear + 10; + const allYears = Array.from( + { length: maxPossibleYear - minPossibleYear + 1 }, + (_, i) => minPossibleYear + i + ); + + // State to control the year picker popup + const [showYearPicker, setShowYearPicker] = useState(false); + const [activePickerType, setActivePickerType] = useState<'min' | 'max' | null>(null); + + // State for drag handling + const [isDragging, setIsDragging] = useState(false); + const [currentThumb, setCurrentThumb] = useState<'min' | 'max' | null>(null); + + // References to slider elements + const yearPickerRef = useRef(null); + const sliderRef = useRef(null); + + // Effect to handle clicks outside the year picker + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (yearPickerRef.current && !yearPickerRef.current.contains(event.target as Node)) { + setShowYearPicker(false); + } + } + + // Add event listener when the picker is shown + if (showYearPicker) { + document.addEventListener('mousedown', handleClickOutside); + } + + // Clean up + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [showYearPicker]); + + // Function to handle mouse down on thumb elements + const handleThumbMouseDown = (event: React.MouseEvent, thumbType: 'min' | 'max') => { + // Prevent default and stop propagation + event.preventDefault(); + event.stopPropagation(); + + // Remove focus from any active element to prevent keyboard events + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + + // Set dragging state immediately + setCurrentThumb(thumbType); + setIsDragging(true); + + // Add dragging styles and prevent text selection globally + document.body.classList.add('slider-dragging'); + document.body.style.userSelect = 'none'; + document.body.style.cursor = 'grabbing'; + + // Capture the initial position + const sliderRect = sliderRef.current?.getBoundingClientRect(); + if (sliderRect) { + const position = (event.clientX - sliderRect.left) / sliderRect.width; + const year = Math.round(minPossibleYear + position * (maxPossibleYear - minPossibleYear)); + if (thumbType === 'min') { + setYearRangeFilter(prev => ({ + ...prev, + min: Math.min(year, prev.max) + })); + } else { + setYearRangeFilter(prev => ({ + ...prev, + max: Math.max(year, prev.min) + })); + } + } + + // Add the event listeners right away + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleEnd); + }; + + // Function to handle touch events for mobile devices + const handleThumbTouchStart = (event: React.TouchEvent, thumbType: 'min' | 'max') => { + // Prevent default to avoid scrolling + event.preventDefault(); + event.stopPropagation(); + + // Set dragging state immediately + setCurrentThumb(thumbType); + setIsDragging(true); + + // Add dragging styles and prevent text selection globally + document.body.classList.add('slider-dragging'); + document.body.style.userSelect = 'none'; + document.body.style.touchAction = 'none'; + + // Capture the initial position + const sliderRect = sliderRef.current?.getBoundingClientRect(); + if (sliderRect && event.touches[0]) { + const position = (event.touches[0].clientX - sliderRect.left) / sliderRect.width; + const year = Math.round(minPossibleYear + position * (maxPossibleYear - minPossibleYear)); + if (thumbType === 'min') { + setYearRangeFilter(prev => ({ + ...prev, + min: Math.min(year, prev.max) + })); + } else { + setYearRangeFilter(prev => ({ + ...prev, + max: Math.max(year, prev.min) + })); + } + } + + // Add the event listeners right away + document.addEventListener('touchmove', handleTouchMove, { passive: false }); + document.addEventListener('touchend', handleEnd); + document.addEventListener('touchcancel', handleEnd); + if (sliderRect && event.touches[0]) { + const position = (event.touches[0].clientX - sliderRect.left) / sliderRect.width; + const year = Math.round(minPossibleYear + position * (maxPossibleYear - minPossibleYear)); + setYearRangeFilter(prev => ({ + ...prev, + [thumbType]: thumbType === 'min' ? Math.min(year, prev.max) : Math.max(year, prev.min) + })); + } + }; + + // Functions for handling drag movement and end + const updateThumbPosition = (clientX: number) => { + if (!sliderRef.current || !currentThumb) return; + + // Get slider dimensions + const sliderRect = sliderRef.current.getBoundingClientRect(); + const sliderWidth = sliderRect.width; + const sliderLeft = sliderRect.left; + + // Calculate position as a percentage of the slider width + let position = (clientX - sliderLeft) / sliderWidth; + position = Math.min(1, Math.max(0, position)); // Clamp between 0 and 1 + + // Convert position to year + const year = Math.round(minPossibleYear + position * (maxPossibleYear - minPossibleYear)); + + // Update the appropriate year based on which thumb is being dragged + if (currentThumb === 'min') { + setYearRangeFilter(prev => ({ + ...prev, + min: Math.min(year, prev.max) + })); + } else { + setYearRangeFilter(prev => ({ + ...prev, + max: Math.max(year, prev.min) + })); + } + }; + + // Handler for mouse movement (defined outside useEffect for direct binding) + const handleMouseMove = (event: MouseEvent) => { + if (!isDragging) return; + event.preventDefault(); + requestAnimationFrame(() => updateThumbPosition(event.clientX)); + }; + + // Handler for touch movement + const handleTouchMove = (event: TouchEvent) => { + if (!isDragging || !event.touches[0]) return; + event.preventDefault(); + requestAnimationFrame(() => updateThumbPosition(event.touches[0].clientX)); + }; + + // Handler for ending the drag + const handleEnd = (event?: MouseEvent | TouchEvent) => { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + // Remove event listeners + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleEnd); + document.removeEventListener('touchmove', handleTouchMove); + document.removeEventListener('touchend', handleEnd); + document.removeEventListener('touchcancel', handleEnd); + + // Reset state and styles + setIsDragging(false); + setCurrentThumb(null); + document.body.classList.remove('slider-dragging'); + document.body.style.userSelect = ''; + document.body.style.cursor = ''; + document.body.style.touchAction = ''; + }; + + // Clean up event listeners on unmount + useEffect(() => { + return () => { + if (isDragging) { + handleEnd(); + } + }; + }, []); + + // Function to handle direct clicks on the slider track + const handleTrackClick = (event: React.MouseEvent) => { + if (sliderRef.current) { + // Get slider dimensions + const sliderRect = sliderRef.current.getBoundingClientRect(); + const sliderWidth = sliderRect.width; + const sliderLeft = sliderRect.left; + + // Calculate position as a percentage of the slider width + let position = (event.clientX - sliderLeft) / sliderWidth; + position = Math.min(1, Math.max(0, position)); // Clamp between 0 and 1 + + // Convert position to year + const year = Math.round(minPossibleYear + position * (maxPossibleYear - minPossibleYear)); + + // Calculate current thumb positions as percentages + const minPos = minThumbPosition / 100; // Convert percentage to decimal + const maxPos = maxThumbPosition / 100; + + // Create a visual ripple effect at click position + const clickEffect = document.createElement('div'); + clickEffect.className = 'absolute w-4 h-4 bg-blue-400 rounded-full transform -translate-x-1/2 -translate-y-1/2 animate-ping'; + clickEffect.style.left = `${event.clientX - sliderRect.left}px`; + clickEffect.style.top = '50%'; + clickEffect.style.opacity = '0.6'; + clickEffect.style.pointerEvents = 'none'; + sliderRef.current.appendChild(clickEffect); + + // Remove the effect after animation + setTimeout(() => { + if (sliderRef.current && sliderRef.current.contains(clickEffect)) { + sliderRef.current.removeChild(clickEffect); + } + }, 500); + + // Determine which thumb to move based on which side of the range the click is on + let thumbType: 'min' | 'max' = 'min'; + + if (position <= minPos) { + // If click is to left of min thumb, move min thumb + thumbType = 'min'; + setYearRangeFilter(prev => ({ + ...prev, + min: year + })); + } else if (position >= maxPos) { + // If click is to right of max thumb, move max thumb + thumbType = 'max'; + setYearRangeFilter(prev => ({ + ...prev, + max: year + })); + } else { + // If click is inside range, move whichever thumb is closer + const minDistance = Math.abs(position - minPos); + const maxDistance = Math.abs(position - maxPos); + + if (minDistance <= maxDistance) { + thumbType = 'min'; + setYearRangeFilter(prev => ({ + ...prev, + min: year + })); + } else { + thumbType = 'max'; + setYearRangeFilter(prev => ({ + ...prev, + max: year + })); + } + } + + // Animate the thumb that was moved + setCurrentThumb(thumbType); + setTimeout(() => { + setCurrentThumb(null); + }, 300); + } + }; + + // Function to handle touch events on the slider track + const handleTrackTouch = (event: React.TouchEvent) => { + if (sliderRef.current && event.touches[0]) { + // Prevent default to avoid scrolling while interacting + event.preventDefault(); + + // Get slider dimensions + const sliderRect = sliderRef.current.getBoundingClientRect(); + const sliderWidth = sliderRect.width; + const sliderLeft = sliderRect.left; + + // Calculate position as a percentage of the slider width + let position = (event.touches[0].clientX - sliderLeft) / sliderWidth; + position = Math.min(1, Math.max(0, position)); // Clamp between 0 and 1 + + // Convert position to year + const year = Math.round(minPossibleYear + position * (maxPossibleYear - minPossibleYear)); + + // Use the same logic as the click handler + const minPos = minThumbPosition / 100; + const maxPos = maxThumbPosition / 100; + + if (position <= minPos) { + setYearRangeFilter(prev => ({ + ...prev, + min: year + })); + } else if (position >= maxPos) { + setYearRangeFilter(prev => ({ + ...prev, + max: year + })); + } else { + const minDistance = Math.abs(position - minPos); + const maxDistance = Math.abs(position - maxPos); + + if (minDistance <= maxDistance) { + setYearRangeFilter(prev => ({ + ...prev, + min: year + })); + } else { + setYearRangeFilter(prev => ({ + ...prev, + max: year + })); + } + } + } + }; + + // Function to toggle the year picker and set the active type + const toggleYearPicker = (type: 'min' | 'max') => { + setActivePickerType(type); + setShowYearPicker(!showYearPicker && type === activePickerType ? false : true); + }; + + // Function to select a year + const selectYear = (year: number) => { + if (activePickerType === 'min') { + // Update the min year + setYearRangeFilter(prev => ({ + ...prev, + min: Math.min(year, prev.max) + })); + + // Automatically switch to max year picker + // No delay needed as we're not closing the popup + setActivePickerType('max'); + } else if (activePickerType === 'max') { + setYearRangeFilter(prev => ({ + ...prev, + max: Math.max(year, prev.min) + })); + setShowYearPicker(false); + } + }; + + // Calculate the percentage for the range slider thumbs + const minThumbPosition = ((yearRangeFilter.min - minPossibleYear) / (maxPossibleYear - minPossibleYear)) * 100; + const maxThumbPosition = ((yearRangeFilter.max - minPossibleYear) / (maxPossibleYear - minPossibleYear)) * 100; + + return ( +
+ {/* Year display with buttons */} +
+ + to + +
+ + {/* Range Slider */} +
+
+ {/* Filled area between thumbs */} +
+ + + {/* Min thumb */} +
handleThumbMouseDown(e, 'min')} + onTouchStart={(e) => handleThumbTouchStart(e, 'min')} + onClick={(e) => { + e.stopPropagation(); + if (!isDragging) { + toggleYearPicker('min'); + } + }} + > + {/* Drag handle indicators */} +
+
+
+
+
+
+
+ +
+ + {/* Max thumb */} +
handleThumbMouseDown(e, 'max')} + onTouchStart={(e) => handleThumbTouchStart(e, 'max')} + onClick={(e) => { + e.stopPropagation(); + if (!isDragging) { + toggleYearPicker('max'); + } + }} + > + {/* Drag handle indicators */} +
+
+
+
+
+
+
+ +
+
+ + {/* Year labels */} +
+ {minPossibleYear} + {maxPossibleYear} +
+
+ + {/* Year picker popup */} + {showYearPicker && ( +
+
+ {allYears.map(year => ( + + ))} +
+
+ )} + +
+
+ Projects from {yearRangeFilter.min} to {yearRangeFilter.max} will be shown +
+
+
+
+
+ ); + }; + const DomainFilterOptions = ({ filter, isSelected, diff --git a/lib/firebase/auth.ts b/lib/firebase/auth.ts index 2bc834d..132fda6 100644 --- a/lib/firebase/auth.ts +++ b/lib/firebase/auth.ts @@ -1,39 +1,46 @@ // Import User and other necessary types/functions from Firebase Auth import { signInWithPopup, GoogleAuthProvider, User, UserCredential } from 'firebase/auth'; import { doc, getDoc } from 'firebase/firestore'; -import { auth, firestore } from './config'; // Ensure you are correctly importing from your Firebase config +import { getFirebaseAuth, getFirebaseFirestore, isFirebaseInitialized } from './config'; // Function to track authentication state changes export function onAuthStateChanged(callback: (authUser: User | null) => void) { - return auth.onAuthStateChanged(callback); + if (!isFirebaseInitialized()) { + // If Firebase is not initialized, always return null user + setTimeout(() => callback(null), 0); + return () => {}; // Return a no-op unsubscribe function + } + return getFirebaseAuth().onAuthStateChanged(callback); } // Function for Google sign-in and role check export async function signInWithGoogle(): Promise<{ isAdmin: boolean }> { + if (!isFirebaseInitialized()) { + console.warn('Firebase is not initialized. Running in development mode.'); + return { isAdmin: false }; + } + const provider = new GoogleAuthProvider(); provider.setCustomParameters({ display: "popup" }); // Force popup - try { - const result: UserCredential = await signInWithPopup(auth, provider); + const result: UserCredential = await signInWithPopup(getFirebaseAuth(), provider); const user: User = result.user; if (!user || !user.email) { throw new Error('Google sign-in failed'); } - // Restrict login to only emails from "gecskp.ac.in" // Restrict login to only emails from "gecskp.ac.in", except for a specific admin email -const allowedEmailPattern = /^[a-zA-Z0-9]+@gecskp\.ac\.in$/; -const adminOverrideEmail = "codecompass2024@gmail.com"; - -if (user.email !== adminOverrideEmail && !allowedEmailPattern.test(user.email)) { - throw new Error('Only GEC SKP emails are allowed'); -} + const allowedEmailPattern = /^[a-zA-Z0-9]+@gecskp\.ac\.in$/; + const adminOverrideEmail = "codecompass2024@gmail.com"; - + if (user.email !== adminOverrideEmail && !allowedEmailPattern.test(user.email)) { + await getFirebaseAuth().signOut(); // Sign out the user if email is not allowed + throw new Error('Only GEC SKP emails are allowed'); + } - const userDocRef = doc(firestore, 'adminemail', user.email); + const userDocRef = doc(getFirebaseFirestore(), 'adminemail', user.email); const userDoc = await getDoc(userDocRef); const isAdmin = userDoc.exists() && userDoc.data()?.role === 'admin'; @@ -45,11 +52,14 @@ if (user.email !== adminOverrideEmail && !allowedEmailPattern.test(user.email)) } } - - export async function signOutWithGoogle(): Promise { + if (!isFirebaseInitialized()) { + console.warn('Firebase is not initialized. No need to sign out.'); + return; + } + try { - await auth.signOut(); + await getFirebaseAuth().signOut(); } catch (error) { console.error('Error signing out with Google:', error); throw error; diff --git a/lib/firebase/config.ts b/lib/firebase/config.ts index 0c28610..36a69f3 100644 --- a/lib/firebase/config.ts +++ b/lib/firebase/config.ts @@ -1,29 +1,126 @@ import { initializeApp, getApps, FirebaseApp } from 'firebase/app'; -import { getAuth } from 'firebase/auth'; -import { getFirestore } from 'firebase/firestore'; -import { getStorage, ref, uploadBytes, getDownloadURL } from 'firebase/storage'; - -const firebaseConfig = { - apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, - authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, - projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, - storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, - messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, - appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, - measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, +import { getAuth, Auth } from 'firebase/auth'; +import { getFirestore, Firestore } from 'firebase/firestore'; +import { getStorage, ref, uploadBytes, getDownloadURL, FirebaseStorage } from 'firebase/storage'; + +// Function to check if all required Firebase config values are present +const hasValidFirebaseConfig = () => { + const requiredKeys = [ + 'NEXT_PUBLIC_FIREBASE_API_KEY', + 'NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN', + 'NEXT_PUBLIC_FIREBASE_PROJECT_ID', + 'NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET', + 'NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID', + 'NEXT_PUBLIC_FIREBASE_APP_ID' + ]; + + return requiredKeys.every(key => !!process.env[key]); +}; + +// Mock implementations for when Firebase is not configured +const createMockAuth = (): Auth => ({ + currentUser: null, + onAuthStateChanged: (callback: any) => { + callback(null); + return () => {}; + }, + signOut: async () => Promise.resolve(), +} as unknown as Auth); + +const createMockStorage = (app: FirebaseApp): FirebaseStorage => ({ + app, + maxUploadRetryTime: 600000, + maxOperationRetryTime: 600000, + ref: () => ({}), +} as unknown as FirebaseStorage); + +// Initialize Firebase or mock services based on environment and config +let firebaseApp: FirebaseApp; +let auth: Auth; +let firestore: Firestore; +let storage: FirebaseStorage; + +// Lazy initialization of Firebase services +const initializeFirebaseServices = () => { + // Return early if already initialized + if (firebaseApp) return; + + // Use mock services in development when config is missing + if (!hasValidFirebaseConfig()) { + if (process.env.NODE_ENV === 'development') { + console.warn( + 'Firebase configuration is incomplete. Running in development mode with mock services.\n' + + 'To use real Firebase services, ensure all required environment variables are set in your .env.local file.' + ); + firebaseApp = {} as FirebaseApp; + auth = createMockAuth(); + firestore = {} as Firestore; + storage = createMockStorage(firebaseApp); + return; + } else { + throw new Error('Firebase configuration is required but missing.'); + } + } + + // Initialize Firebase with real services + try { + const firebaseConfig = { + apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, + measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, + }; + + // Initialize Firebase only if it hasn't been initialized yet + firebaseApp = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; + + // Initialize services + auth = getAuth(firebaseApp); + firestore = getFirestore(firebaseApp); + storage = getStorage(firebaseApp); + } catch (error) { + console.warn('Failed to initialize Firebase:', error); + // Keep using mock implementations if initialization fails + } +} else if (process.env.NODE_ENV === 'development') { + console.warn( + 'Firebase configuration is incomplete. Running in development mode with mock services.\n' + + 'To use real Firebase services, ensure all required environment variables are set in your .env.local file.' + ); +} + +// Export getter functions that ensure Firebase is initialized when needed +export const getFirebaseApp = () => { + initializeFirebaseServices(); + return firebaseApp; }; +export const getFirebaseAuth = () => { + initializeFirebaseServices(); + return auth; +}; +export const getFirebaseFirestore = () => { + initializeFirebaseServices(); + return firestore; +}; -// Initialize Firebase app -export const firebaseApp: FirebaseApp = - getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; +export const getFirebaseStorage = () => { + initializeFirebaseServices(); + return storage; +}; -export const auth = getAuth(firebaseApp); -export const firestore = getFirestore(firebaseApp); -export const storage = getStorage(firebaseApp); +// Helper function to check if Firebase is properly initialized +export const isFirebaseInitialized = () => getApps().length > 0; export async function uploadFileToFirebase(file: File, folder: string): Promise { + if (!isFirebaseInitialized()) { + throw new Error('Firebase is not initialized. Please check your configuration.'); + } + const storageRef = ref(storage, `${folder}/${file.name}`); const snapshot = await uploadBytes(storageRef, file); return await getDownloadURL(snapshot.ref);