diff --git a/database-setup.sql b/database-setup.sql index b93639a..b547fc3 100644 --- a/database-setup.sql +++ b/database-setup.sql @@ -25,12 +25,31 @@ CREATE TABLE IF NOT EXISTS public.quiz_history ( created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); +-- Create bookmarked_questions table to store user's saved questions +CREATE TABLE IF NOT EXISTS public.bookmarked_questions ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL, + question TEXT NOT NULL, + options JSONB NOT NULL, -- array of answer options + correct_answer TEXT NOT NULL, + explanation TEXT, + category TEXT NOT NULL, + difficulty TEXT NOT NULL, + notes TEXT, -- user's personal notes about the question + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- Ensure uniqueness: same user cannot bookmark the same question twice + UNIQUE(user_id, question, correct_answer) +); + -- Enable RLS on profiles table ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; -- Enable RLS on quiz_history table ALTER TABLE public.quiz_history ENABLE ROW LEVEL SECURITY; +-- Enable RLS on bookmarked_questions table +ALTER TABLE public.bookmarked_questions ENABLE ROW LEVEL SECURITY; + -- Create policies for profiles table CREATE POLICY "Users can view own profile" ON public.profiles FOR SELECT USING (auth.uid() = id); @@ -48,6 +67,19 @@ CREATE POLICY "Users can view own quiz history" ON public.quiz_history CREATE POLICY "Users can insert own quiz history" ON public.quiz_history FOR INSERT WITH CHECK (auth.uid() = user_id); +-- Create policies for bookmarked_questions table +CREATE POLICY "Users can view own bookmarks" ON public.bookmarked_questions + FOR SELECT USING (auth.uid() = user_id); + +CREATE POLICY "Users can insert own bookmarks" ON public.bookmarked_questions + FOR INSERT WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "Users can update own bookmarks" ON public.bookmarked_questions + FOR UPDATE USING (auth.uid() = user_id); + +CREATE POLICY "Users can delete own bookmarks" ON public.bookmarked_questions + FOR DELETE USING (auth.uid() = user_id); + -- Create function to automatically create profile on user signup CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS TRIGGER AS $$ @@ -68,3 +100,8 @@ CREATE TRIGGER on_auth_user_created CREATE INDEX IF NOT EXISTS idx_quiz_history_user_id ON public.quiz_history(user_id); CREATE INDEX IF NOT EXISTS idx_quiz_history_created_at ON public.quiz_history(created_at DESC); CREATE INDEX IF NOT EXISTS idx_quiz_history_category ON public.quiz_history(category); + +-- Create indexes for bookmarked_questions +CREATE INDEX IF NOT EXISTS idx_bookmarked_questions_user_id ON public.bookmarked_questions(user_id); +CREATE INDEX IF NOT EXISTS idx_bookmarked_questions_created_at ON public.bookmarked_questions(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_bookmarked_questions_category ON public.bookmarked_questions(category); diff --git a/package.json b/package.json index f200ab6..7a5dcc7 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { + "dev": "vite", "build": "vite build", "lint": "eslint .", "preview": "vite preview", diff --git a/server.js b/server.js index 66f330b..0c5c223 100644 --- a/server.js +++ b/server.js @@ -11,7 +11,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); -const PORT = process.env.PORT || 5174; +const PORT = process.env.PORT || 3001; // Changed from 5174 to 3001 app.use(cors()); app.use(express.json()); diff --git a/src/App.jsx b/src/App.jsx index 225ccee..2a75f1f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,6 +8,7 @@ import NotificationBadge from "./components/NotificationBadge"; import GlassmorphicDropdown from "./components/GlassmorphicDropdown"; import { AuthProvider, useAuth } from "./contexts/AuthContext"; import { quizService } from "./services/quizService"; +import { bookmarkService } from "./services/bookmarkService"; import { jsPDF } from 'jspdf'; // Import jsPDF import './components/Result.css' @@ -374,6 +375,10 @@ export default function App({ user, onSignIn, onSignUp, onSignOut, onShowDashboa const [showStartScreen, setShowStartScreen] = useState(true); const [originalQuiz, setOriginalQuiz] = useState([]); const [showExamPrepPage, setShowExamPrepPage] = useState(false); + + // Bookmark states + const [bookmarkedQuestions, setBookmarkedQuestions] = useState(new Set()); + const [bookmarkLoading, setBookmarkLoading] = useState(new Set()); const [isDarkMode, setIsDarkMode] = useState(() => { @@ -559,6 +564,88 @@ export default function App({ user, onSignIn, onSignUp, onSignOut, onShowDashboa setShowStartScreen(true); } + // Bookmark functions + const checkBookmarkStatus = async (question) => { + if (!user) return; + + try { + const result = await bookmarkService.isBookmarked(question.question, question.answer); + if (result.isBookmarked) { + setBookmarkedQuestions(prev => new Set([...prev, `${question.question}-${question.answer}`])); + } + } catch (error) { + console.error('Error checking bookmark status:', error); + } + }; + + const handleBookmarkToggle = async (questionIndex) => { + if (!user) { + // Show sign-in prompt or modal + alert('Please sign in to bookmark questions'); + return; + } + + const question = quiz[questionIndex]; + const questionKey = `${question.question}-${question.answer}`; + + setBookmarkLoading(prev => new Set([...prev, questionIndex])); + + try { + const isCurrentlyBookmarked = bookmarkedQuestions.has(questionKey); + + if (isCurrentlyBookmarked) { + // Remove bookmark + const result = await bookmarkService.removeBookmarkByQuestion(question.question, question.answer); + if (result.success) { + setBookmarkedQuestions(prev => { + const newSet = new Set(prev); + newSet.delete(questionKey); + return newSet; + }); + } else { + console.error('Error removing bookmark:', result.error); + } + } else { + // Add bookmark + const bookmarkData = { + question: question.question, + options: question.options, + answer: question.answer, + explanation: question.explanation, + category: selectedCategory, + difficulty: selectedDifficulty + }; + + const result = await bookmarkService.saveBookmark(bookmarkData); + if (result.data) { + setBookmarkedQuestions(prev => new Set([...prev, questionKey])); + } else if (result.error === 'Question already bookmarked') { + // Question was already bookmarked, update UI state + setBookmarkedQuestions(prev => new Set([...prev, questionKey])); + } else { + console.error('Error saving bookmark:', result.error); + } + } + } catch (error) { + console.error('Error toggling bookmark:', error); + } finally { + setBookmarkLoading(prev => { + const newSet = new Set(prev); + newSet.delete(questionIndex); + return newSet; + }); + } + }; + + // Check bookmark status when quiz loads + useEffect(() => { + if (quiz.length > 0 && user) { + quiz.forEach(question => { + checkBookmarkStatus(question); + }); + } + }, [quiz, user]); + // PDF Generation Function const generatePDF = () => { try { @@ -1044,8 +1131,32 @@ export default function App({ user, onSignIn, onSignUp, onSignOut, onShowDashboa {quiz.map((q, idx) => (
- Q{idx + 1} -

{q.question}

+
+ Q{idx + 1} +

{q.question}

+
+ {user && ( + + )}
{q.options.map((opt, i) => ( diff --git a/src/components/BookmarkedQuestions.jsx b/src/components/BookmarkedQuestions.jsx new file mode 100644 index 0000000..c4ce40c --- /dev/null +++ b/src/components/BookmarkedQuestions.jsx @@ -0,0 +1,378 @@ +import { useState, useEffect } from 'react' +import { bookmarkService } from '../services/bookmarkService' +import GlassmorphicDropdown from './GlassmorphicDropdown' + +const BookmarkedQuestions = ({ user }) => { + const [bookmarks, setBookmarks] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [stats, setStats] = useState(null) + const [selectedCategory, setSelectedCategory] = useState('All Categories') + const [searchTerm, setSearchTerm] = useState('') + const [editingNotes, setEditingNotes] = useState(null) + const [noteText, setNoteText] = useState('') + + useEffect(() => { + if (user) { + loadBookmarks() + loadBookmarkStats() + } + }, [user]) + + useEffect(() => { + if (user) { + filterBookmarks() + } + }, [selectedCategory, searchTerm]) + + const loadBookmarks = async () => { + setLoading(true) + setError(null) + + try { + const result = await bookmarkService.getUserBookmarks(100) // Load up to 100 bookmarks + if (result.error) throw new Error(result.error) + setBookmarks(result.data || []) + } catch (err) { + console.error('Error loading bookmarks:', err) + setError('Failed to load bookmarked questions.') + } finally { + setLoading(false) + } + } + + const loadBookmarkStats = async () => { + try { + const result = await bookmarkService.getBookmarkStats() + if (result.error) throw new Error(result.error) + setStats(result.data) + } catch (err) { + console.error('Error loading bookmark stats:', err) + } + } + + const filterBookmarks = async () => { + if (!user) return + + setLoading(true) + try { + let result + + if (searchTerm.trim()) { + // Search bookmarks + result = await bookmarkService.searchBookmarks(searchTerm, 50) + } else if (selectedCategory !== 'All Categories') { + // Filter by category + result = await bookmarkService.getBookmarksByCategory(selectedCategory, 50) + } else { + // Load all bookmarks + result = await bookmarkService.getUserBookmarks(100) + } + + if (result.error) throw new Error(result.error) + setBookmarks(result.data || []) + } catch (err) { + console.error('Error filtering bookmarks:', err) + setError('Failed to filter bookmarks.') + } finally { + setLoading(false) + } + } + + const handleRemoveBookmark = async (bookmarkId) => { + try { + const result = await bookmarkService.removeBookmark(bookmarkId) + if (result.error) throw new Error(result.error) + + setBookmarks(prev => prev.filter(bookmark => bookmark.id !== bookmarkId)) + loadBookmarkStats() // Refresh stats + } catch (err) { + console.error('Error removing bookmark:', err) + alert('Failed to remove bookmark. Please try again.') + } + } + + const handleUpdateNotes = async (bookmarkId) => { + try { + const result = await bookmarkService.updateBookmarkNotes(bookmarkId, noteText) + if (result.error) throw new Error(result.error) + + setBookmarks(prev => + prev.map(bookmark => + bookmark.id === bookmarkId + ? { ...bookmark, notes: noteText } + : bookmark + ) + ) + setEditingNotes(null) + setNoteText('') + } catch (err) { + console.error('Error updating notes:', err) + alert('Failed to update notes. Please try again.') + } + } + + const startEditingNotes = (bookmark) => { + setEditingNotes(bookmark.id) + setNoteText(bookmark.notes || '') + } + + const cancelEditingNotes = () => { + setEditingNotes(null) + setNoteText('') + } + + const formatDate = (dateString) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + } + + const getDifficultyColor = (difficulty) => { + switch (difficulty?.toLowerCase()) { + case 'easy': return 'text-green-400' + case 'medium': return 'text-yellow-400' + case 'hard': return 'text-red-400' + default: return 'text-gray-400' + } + } + + const getCategoryOptions = () => { + if (!stats?.categoryCounts) return ['All Categories'] + return ['All Categories', ...Object.keys(stats.categoryCounts)] + } + + if (loading && bookmarks.length === 0) { + return ( +
+
+

Loading your bookmarked questions...

+
+ ) + } + + if (error) { + return ( +
+
+

Error Loading Bookmarks

+

{error}

+ +
+ ) + } + + return ( +
+ {/* Stats Overview */} + {stats && ( +
+

+ 🔖 + Bookmark Statistics +

+ +
+
+
{stats.totalBookmarks}
+
Total Bookmarks
+
+
+
{stats.recentBookmarks}
+
This Week
+
+
+
+ {Object.keys(stats.categoryCounts).length} +
+
Categories
+
+
+
+ {Object.entries(stats.categoryCounts).reduce((max, [cat, count]) => + count > max.count ? { category: cat, count } : max, + { category: 'None', count: 0 } + ).category} +
+
Top Category
+
+
+
+ )} + + {/* Filters */} +
+
+ + +
+ +
+ + setSearchTerm(e.target.value)} + placeholder="Search questions, explanations, or notes..." + className="flex-1 px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500/50" + /> +
+
+ + {/* Bookmarks List */} + {bookmarks.length === 0 ? ( +
+
📌
+

+ {searchTerm || selectedCategory !== 'All Categories' + ? 'No matching bookmarks found' + : 'No Bookmarked Questions' + } +

+

+ {searchTerm || selectedCategory !== 'All Categories' + ? 'Try adjusting your filters or search terms.' + : 'Start bookmarking interesting questions during quizzes to see them here!' + } +

+
+ ) : ( +
+ {bookmarks.map((bookmark) => ( +
+ {/* Question Header */} +
+
+
+ + {bookmark.category} + + + {bookmark.difficulty} + + + {formatDate(bookmark.created_at)} + +
+

{bookmark.question}

+
+ +
+ + {/* Options */} +
+ {bookmark.options.map((option, index) => ( +
+ + {String.fromCharCode(65 + index)}. + + {option} + {option === bookmark.correct_answer && ( + + )} +
+ ))} +
+ + {/* Explanation */} + {bookmark.explanation && ( +
+
Explanation:
+

{bookmark.explanation}

+
+ )} + + {/* Notes Section */} +
+
+
Personal Notes:
+ {editingNotes !== bookmark.id && ( + + )} +
+ + {editingNotes === bookmark.id ? ( +
+