diff --git a/chat.html b/chat.html new file mode 100644 index 0000000..9459086 --- /dev/null +++ b/chat.html @@ -0,0 +1,99 @@ + + + + + + Scholarly - AI Study Buddy + + + + + + + +
+
+

🤖 AI Study Buddy

+ +
+ + +
+
+ +
+
+

Hello! I'm Scholarly, your AI Study Buddy! 📚

+

I'm here to help you with your studies. I know you're studying .

+

What would you like to learn about today?

+
+
+ +
+ + +
+ + +
+ + + + + +
+
+ + + + + + \ No newline at end of file diff --git a/chat.js b/chat.js new file mode 100644 index 0000000..58a0342 --- /dev/null +++ b/chat.js @@ -0,0 +1,177 @@ +// File: chat.js (Complete and Final Code) +document.addEventListener('DOMContentLoaded', () => { + const API_URL = "http://localhost:5000"; + + // State variables + let userInfo = null; + let studentInfo = null; + let chatHistory = JSON.parse(localStorage.getItem('chatHistory')) || []; + let currentSubject = ''; + + // DOM Elements + const chatWindow = document.getElementById('chat-window'); + const chatInput = document.getElementById('chat-input'); + const sendBtn = document.getElementById('send-btn'); + const subjectSelect = document.getElementById('chat-subject'); + const quickPrompts = document.querySelectorAll('.quick-prompt'); + + async function init() { + try { + // Securely fetch user data from the server based on the session cookie + const meResponse = await fetch(`${API_URL}/api/me`, { credentials: 'include' }); + if (!meResponse.ok) { + // If not authenticated, redirect to the login page + window.location.href = 'login.html'; + return; + } + const meData = await meResponse.json(); + userInfo = meData.loggedInUser; + studentInfo = meData.studentInfo; + + // Now that we are authenticated, load the chat content + displayStudentInfo(); + await loadSubjects(); + loadChatHistory(); + setupEventListeners(); + } catch (error) { + console.error("Chat Initialization Error:", error); + window.location.href = 'login.html'; + } + } + + function displayStudentInfo() { + if (studentInfo) { + const infoText = `${studentInfo.board} Board, Class ${studentInfo.class}${studentInfo.stream ? ' (' + studentInfo.stream + ')' : ''}`; + document.getElementById('student-info-chat').textContent = infoText; + } + } + + async function loadSubjects() { + try { + const res = await fetch(`${API_URL}/api/subjects/${studentInfo?.class}/${studentInfo?.stream || ''}`); + if (res.ok) { + const subjects = await res.json(); + const customSubjects = JSON.parse(localStorage.getItem('customSubjects')) || []; + [...subjects, ...customSubjects].forEach(subject => { + const option = document.createElement('option'); + option.value = subject; + option.textContent = subject; + subjectSelect.appendChild(option); + }); + } + } catch (err) { + console.error('Error loading subjects:', err); + } + } + + function loadChatHistory() { + chatHistory.slice(-10).forEach(msg => appendMessage(msg.text, msg.sender, false)); + if (chatHistory.length > 0) chatWindow.scrollTop = chatWindow.scrollHeight; + } + + async function sendMessage() { + const messageText = chatInput.value.trim(); + if (!messageText) return; + + appendMessage(messageText, 'user'); + chatInput.value = ''; + chatInput.style.height = 'auto'; + + const typingIndicator = showTypingIndicator(); + + try { + const res = await fetch(`${API_URL}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ message: messageText, subject: currentSubject || null }) + }); + + if (!res.ok) throw new Error('Server error'); + const data = await res.json(); + + chatWindow.removeChild(typingIndicator); + appendMessage(data.reply, 'bot'); + + } catch (error) { + console.error('Chat error:', error); + if (typingIndicator?.parentNode) chatWindow.removeChild(typingIndicator); + appendMessage("Sorry, I couldn't connect to the server. Please try again later.", 'bot'); + } + } + + function appendMessage(text, sender, save = true) { + const messageDiv = document.createElement('div'); + messageDiv.classList.add('message', `${sender}-message`); + messageDiv.innerHTML = formatMessage(text); + chatWindow.appendChild(messageDiv); + chatWindow.scrollTop = chatWindow.scrollHeight; + + if (save) { + chatHistory.push({ text, sender }); + localStorage.setItem('chatHistory', JSON.stringify(chatHistory.slice(-50))); + } + } + + function formatMessage(text) { + return text.split('\n').filter(p => p.trim()).map(p => { + p = p.replace(/\*\*(.*?)\*\*/g, '$1').replace(/\*(.*?)\*/g, '$1').replace(/`(.*?)`/g, '$1'); + if (p.startsWith('- ')) p = '• ' + p.substring(2); + return `

${p}

`; + }).join(''); + } + + function showTypingIndicator() { + const typingDiv = document.createElement('div'); + typingDiv.className = 'message bot-message typing-indicator'; + typingDiv.innerHTML = ''; + chatWindow.appendChild(typingDiv); + chatWindow.scrollTop = chatWindow.scrollHeight; + return typingDiv; + } + + function setupEventListeners() { + sendBtn.addEventListener('click', sendMessage); + chatInput.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); + chatInput.addEventListener('input', () => { chatInput.style.height = 'auto'; chatInput.style.height = `${chatInput.scrollHeight}px`; }); + subjectSelect.addEventListener('change', e => { currentSubject = e.target.value; }); + quickPrompts.forEach(btn => btn.addEventListener('click', () => { chatInput.value += ` ${btn.dataset.prompt}`; chatInput.focus(); })); + + + const clearChatBtn = document.getElementById('clear-chat-btn'); + if (clearChatBtn) { + clearChatBtn.addEventListener('click', async () => { + if (confirm("Are you sure you want to delete your entire chat history? This cannot be undone.")) { + try { + const res = await fetch(`${API_URL}/api/chat/history`, { + method: 'DELETE', + credentials: 'include' + }); + + if (res.ok) { + // Clear the chat window on the frontend + chatWindow.innerHTML = '

Chat history cleared. How can I help you start fresh?

'; + // Clear the history from local storage as well + localStorage.removeItem('chatHistory'); + chatHistory = []; + } else { + alert("Failed to clear chat history."); + } + } catch (error) { + console.error("Clear chat error:", error); + alert("Could not connect to the server to clear history."); + } + } + }); + } + + document.querySelector('.logout-link').addEventListener('click', async e => { + e.preventDefault(); + await fetch(`${API_URL}/logout`, { method: 'POST', credentials: 'include' }); + localStorage.clear(); + window.location.href = 'login.html'; + }); + } + + init(); +}); \ No newline at end of file diff --git a/dashboard.js b/dashboard.js new file mode 100644 index 0000000..fd114b5 --- /dev/null +++ b/dashboard.js @@ -0,0 +1,271 @@ +// File: dashboard.js (Complete and Final Code) +document.addEventListener('DOMContentLoaded', () => { + const API_URL = "http://localhost:5000"; + + // State variables + let userInfo = null, studentInfo = null, player = {}, subjects = [], quests = [], customSubjects = JSON.parse(localStorage.getItem('customSubjects')) || []; + + // DOM Elements + const levelDisplay = document.getElementById('player-level'), xpText = document.getElementById('xp-text'), xpBar = document.getElementById('xp-bar'); + const questTitleInput = document.getElementById('quest-title'), questSubjectSelect = document.getElementById('quest-subject'), questChapterInput = document.getElementById('quest-chapter'), questTopicInput = document.getElementById('quest-topic'), questDueDateInput = document.getElementById('quest-due-date'), questImportanceSelect = document.getElementById('quest-importance'), addQuestBtn = document.getElementById('add-quest-btn'), questListDiv = document.getElementById('quest-list'); + const profileButton = document.getElementById('profile-button'), profileModal = document.getElementById('profile-modal'), closeModalButton = document.querySelector('.close-button'), subjectListDiv = document.getElementById('subject-list'), newSubjectInput = document.getElementById('new-subject-input'), addSubjectModalBtn = document.getElementById('add-subject-modal-btn'); + const filterBtn = document.getElementById('filter-btn'), filterPanel = document.getElementById('filter-panel'), filterSubject = document.getElementById('filter-subject'), filterImportance = document.getElementById('filter-importance'), filterStatus = document.getElementById('filter-status'); + + async function init() { + try { + const meResponse = await fetch(`${API_URL}/api/me`, { credentials: 'include' }); + if (!meResponse.ok) { + window.location.href = 'login.html'; + return; + } + const meData = await meResponse.json(); + userInfo = meData.loggedInUser; + studentInfo = meData.studentInfo; + player = { level: studentInfo?.level || 1, xp: studentInfo?.total_xp || 0, xpToNextLevel: 100 * (studentInfo?.level || 1) }; + + displayStudentInfo(); + await loadSubjects(); + await loadQuests(); + updateUI(); + setupEventListeners(); + } catch (error) { + console.error("Initialization Error:", error); + window.location.href = 'login.html'; + } + } + + function displayStudentInfo() { + if (studentInfo) { + document.getElementById('student-name').textContent = studentInfo.name || userInfo.email.split('@')[0]; + document.getElementById('student-board').textContent = studentInfo.board + ' Board'; + document.getElementById('student-class').textContent = `Class ${studentInfo.class}`; + document.getElementById('profile-name').textContent = studentInfo.name || userInfo.email.split('@')[0]; + document.getElementById('profile-email').textContent = userInfo.email; + document.getElementById('profile-board').textContent = studentInfo.board; + document.getElementById('profile-class').textContent = studentInfo.class; + document.getElementById('profile-stream').textContent = studentInfo.stream || 'N/A'; + document.getElementById('profile-id').textContent = userInfo.student_id; + } + } + + async function loadSubjects() { + try { + const res = await fetch(`${API_URL}/api/subjects/${studentInfo?.class}/${studentInfo?.stream || ''}`); + if (res.ok) { + subjects = await res.json(); + subjects = [...subjects, ...customSubjects]; + populateSubjectsDropdown(); + populateFilterDropdown(); + } + } catch (err) { console.error('Error loading subjects:', err); } + } + + async function loadQuests() { + try { + const res = await fetch(`${API_URL}/quests`, { credentials: 'include' }); + if (res.ok) { + quests = await res.json(); + renderQuests(); + updateQuestStats(); + updateWeeklyStats(); + } + } catch (err) { console.error('Error loading quests:', err); } + } + + function updateUI() { + levelDisplay.textContent = `Level ${player.level}`; + const xpForCurrentLevel = player.xp - (100 * (player.level - 1)); + player.xpToNextLevel = 100 * player.level; + const xpPercentage = (xpForCurrentLevel / 100) * 100; + xpBar.style.width = `${xpPercentage}%`; + xpText.textContent = `${xpForCurrentLevel} / 100 XP`; + renderSubjectsInModal(); + } + + function showNotification(message, type = 'info') { + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + notification.textContent = message; + document.body.appendChild(notification); + setTimeout(() => { + notification.classList.add('fade-out'); + setTimeout(() => notification.remove(), 300); + }, 2500); + } + + function showLevelUpNotification() { + const notification = document.createElement('div'); + notification.className = 'level-up-notification'; + notification.innerHTML = `

🎉 LEVEL UP!

You reached Level ${player.level}!

`; + document.body.appendChild(notification); + setTimeout(() => notification.remove(), 3000); + } + + function renderQuests() { + questListDiv.innerHTML = ''; + let filteredQuests = [...quests]; + if (filterSubject.value) filteredQuests = filteredQuests.filter(q => q.subject === filterSubject.value); + if (filterImportance.value) filteredQuests = filteredQuests.filter(q => q.importance === filterImportance.value); + if (filterStatus.value === 'pending') filteredQuests = filteredQuests.filter(q => !q.completed); + else if (filterStatus.value === 'completed') filteredQuests = filteredQuests.filter(q => q.completed); + + filteredQuests.sort((a, b) => { + if (!a.completed && b.completed) return -1; if (a.completed && !b.completed) return 1; + const dateA = new Date(a.due_date || '9999-12-31'), dateB = new Date(b.due_date || '9999-12-31'); + return dateA - dateB; + }); + + if (filteredQuests.length === 0) { + questListDiv.innerHTML = '

No quests found. Create one to begin your adventure!

'; + return; + } + filteredQuests.forEach(quest => questListDiv.appendChild(createQuestCard(quest))); + } + + function createQuestCard(quest) { + const dueDate = quest.due_date ? new Date(quest.due_date) : null; + const today = new Date(); today.setHours(0, 0, 0, 0); + let dueDateClass = '', dueDateText = 'No due date'; + if (dueDate) { + const daysUntilDue = Math.ceil((dueDate - today) / (1000 * 60 * 60 * 24)); + if (daysUntilDue < 0) { dueDateClass = 'overdue'; dueDateText = `Overdue by ${Math.abs(daysUntilDue)} days`; } + else if (daysUntilDue === 0) { dueDateClass = 'due-today'; dueDateText = 'Due today!'; } + else if (daysUntilDue <= 3) { dueDateClass = 'due-soon'; dueDateText = `Due in ${daysUntilDue} days`; } + else { dueDateText = dueDate.toLocaleDateString(); } + } + const questCard = document.createElement('div'); + questCard.className = `quest-card importance-${quest.importance} ${quest.completed ? 'completed' : ''}`; + questCard.innerHTML = ` +

${quest.title}

${quest.xp_value} XP
+
+ ${quest.subject ? `

${quest.subject}

` : ''} + ${quest.chapter ? `

Chapter: ${quest.chapter}

` : ''} + ${quest.topic ? `

Topic: ${quest.topic}

` : ''} +

${dueDateText}

+
+
+ ${!quest.completed ? `` : ` Completed!`} + +
`; + const completeBtn = questCard.querySelector('.complete-quest-btn'); + if (completeBtn) completeBtn.addEventListener('click', () => completeQuest(quest.id)); + questCard.querySelector('.delete-quest-btn').addEventListener('click', () => deleteQuest(quest.id)); + return questCard; + } + + async function addQuest() { + const questData = { title: questTitleInput.value.trim(), subject: questSubjectSelect.value, chapter: questChapterInput.value.trim(), topic: questTopicInput.value.trim(), dueDate: questDueDateInput.value, importance: questImportanceSelect.value }; + if (!questData.title) return alert('Please enter a quest title!'); + try { + const res = await fetch(`${API_URL}/quests`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(questData) }); + if (res.ok) { + questTitleInput.value = ''; questChapterInput.value = ''; questTopicInput.value = ''; questDueDateInput.value = ''; questSubjectSelect.value = ''; + await loadQuests(); + showNotification('Quest added successfully!', 'success'); + } else { const error = await res.json(); alert('Error: ' + error.error); } + } catch (err) { console.error('Error adding quest:', err); } + } + + async function completeQuest(questId) { + try { + const res = await fetch(`${API_URL}/quests/${questId}/complete`, { method: 'PUT', credentials: 'include' }); + if (res.ok) { + const data = await res.json(); + player.xp = data.total_xp; + if (data.level > player.level) { + player.level = data.level; + showLevelUpNotification(); + } + showNotification(`+${data.xp_earned} XP earned!`, 'xp'); + await loadQuests(); + updateUI(); + } + } catch (err) { console.error('Error completing quest:', err); } + } + + async function deleteQuest(questId) { + if (!confirm('Are you sure you want to delete this quest?')) return; + try { + const res = await fetch(`${API_URL}/quests/${questId}`, { method: 'DELETE', credentials: 'include' }); + if (res.ok) { + await loadQuests(); + showNotification('Quest deleted', 'info'); + } + } catch (err) { console.error('Error deleting quest:', err); } + } + + function updateQuestStats() { + const today = new Date(); today.setHours(0, 0, 0, 0); + const activeQuests = quests.filter(q => !q.completed); + document.getElementById('active-count').textContent = activeQuests.length; + document.getElementById('today-count').textContent = activeQuests.filter(q => q.due_date && new Date(q.due_date).getTime() === today.getTime()).length; + document.getElementById('overdue-count').textContent = activeQuests.filter(q => q.due_date && new Date(q.due_date) < today).length; + } + + function updateWeeklyStats() { + const oneWeekAgo = new Date(); oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); + const weeklyQuests = quests.filter(q => q.completed_at && new Date(q.completed_at) >= oneWeekAgo); + document.getElementById('weekly-quests').textContent = weeklyQuests.length; + document.getElementById('weekly-xp').textContent = weeklyQuests.reduce((sum, q) => sum + (q.xp_value || 0), 0); + } + + function renderSubjectsInModal() { + subjectListDiv.innerHTML = customSubjects.length === 0 ? '

No custom subjects.

' : ''; + customSubjects.forEach((subject, index) => { + const item = document.createElement('div'); + item.className = 'subject-item'; + item.innerHTML = `${subject}`; + subjectListDiv.appendChild(item); + }); + document.querySelectorAll('.delete-subject-btn').forEach(btn => btn.addEventListener('click', e => deleteSubject(e.target.dataset.index))); + } + + function addSubjectFromModal() { + const newSubject = newSubjectInput.value.trim(); + if (newSubject && !subjects.includes(newSubject)) { + customSubjects.push(newSubject); + subjects.push(newSubject); + localStorage.setItem('customSubjects', JSON.stringify(customSubjects)); + newSubjectInput.value = ''; + populateSubjectsDropdown(); populateFilterDropdown(); renderSubjectsInModal(); + } else if (subjects.includes(newSubject)) alert('Subject already exists!'); + } + + function deleteSubject(index) { + const subject = customSubjects[index]; + if (confirm(`Delete "${subject}"?`)) { + customSubjects.splice(index, 1); + subjects = subjects.filter(s => s !== subject); + localStorage.setItem('customSubjects', JSON.stringify(customSubjects)); + populateSubjectsDropdown(); populateFilterDropdown(); renderSubjectsInModal(); + } + } + + function populateSubjectsDropdown() { + const options = subjects.map(s => ``).join(''); + questSubjectSelect.innerHTML = `${options}`; + } + + function populateFilterDropdown() { + const options = subjects.map(s => ``).join(''); + filterSubject.innerHTML = `${options}`; + } + + function setupEventListeners() { + addQuestBtn.addEventListener('click', addQuest); + profileButton.addEventListener('click', () => profileModal.style.display = 'flex'); + closeModalButton.addEventListener('click', () => profileModal.style.display = 'none'); + addSubjectModalBtn.addEventListener('click', addSubjectFromModal); + filterBtn.addEventListener('click', () => filterPanel.style.display = filterPanel.style.display === 'none' ? 'flex' : 'none'); + [filterSubject, filterImportance, filterStatus].forEach(el => el.addEventListener('change', renderQuests)); + document.querySelector('.logout-link').addEventListener('click', async e => { + e.preventDefault(); + await fetch(`${API_URL}/logout`, { method: 'POST', credentials: 'include' }); + localStorage.clear(); + window.location.href = 'login.html'; + }); + window.addEventListener('click', e => { if (e.target === profileModal) profileModal.style.display = 'none'; }); + } + + init(); +}); \ No newline at end of file diff --git a/hello.js b/hello.js new file mode 100644 index 0000000..1b88fb9 --- /dev/null +++ b/hello.js @@ -0,0 +1,487 @@ +import express from "express"; +import bodyParser from "body-parser"; +import { createClient } from "@supabase/supabase-js"; +import cors from "cors"; +import { GoogleGenerativeAI } from "@google/generative-ai"; +import dotenv from "dotenv"; +import bcrypt from "bcryptjs"; +import session from "express-session"; + +dotenv.config(); + +// Initialize Supabase +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_KEY +); + +// Initialize Gemini AI +const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); + +const app = express(); + +// Middleware +app.use(cors({ + origin: ["http://localhost:3000", "http://127.0.0.1:5500"], + credentials: true +})); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); +app.use(express.static('public')); +app.use(session({ + secret: process.env.SESSION_SECRET || 's3YwO7sf6y', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true in production with HTTPS + httpOnly: true, + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Indian Education Boards and Classes +const INDIAN_BOARDS = { + CBSE: "Central Board of Secondary Education", + ICSE: "Indian School Certificate Examinations", + MAHARASHTRA: "Maharashtra State Board", + UP: "Uttar Pradesh Board", + KARNATAKA: "Karnataka Board", + TAMIL_NADU: "Tamil Nadu Board", + WEST_BENGAL: "West Bengal Board" +}; + +const CLASSES = ["6", "7", "8", "9", "10", "11", "12"]; + +const SUBJECTS_BY_CLASS = { + "6-8": ["Mathematics", "Science", "Social Studies", "English", "Hindi", "Sanskrit"], + "9-10": ["Mathematics", "Science", "Social Science", "English", "Hindi", "Sanskrit", "Computer Science"], + "11-12-Science": ["Physics", "Chemistry", "Mathematics", "Biology", "Computer Science", "English"], + "11-12-Commerce": ["Accountancy", "Economics", "Business Studies", "Mathematics", "English"], + "11-12-Arts": ["History", "Political Science", "Geography", "Psychology", "Sociology", "English"] +}; + +// Check DB connection +async function checkDbConnection() { + try { + const { data, error } = await supabase.from("users").select("*").limit(1); + if (error) console.error("❌ Supabase connection failed:", error.message); + else console.log("✅ Supabase connection successful"); + } catch (err) { + console.error("❌ Error checking DB connection:", err.message); + } +} +checkDbConnection(); + +// ============= AUTH ROUTES ============= + +// Signup with password hashing +app.post("/signup", async (req, res) => { + try { + const { email, parent_email, password, studentClass, board, stream } = req.body; + + if (!email || !parent_email || !password || !studentClass || !board) { + return res.status(400).json({ error: "All fields are required" }); + } + + // Check if user exists + const { data: existingUser } = await supabase + .from("users") + .select("*") + .eq("email", email) + .single(); + + if (existingUser) { + return res.status(400).json({ error: "User already exists" }); + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 10); + + // Generate student ID + const student_id = Math.floor(Math.random() * 900000) + 100000; + + // Insert into students table + const { error: studentError } = await supabase.from("students").insert([ + { + student_id, + class: studentClass, + board, + stream: stream || null, + name: email.split('@')[0], // Use email prefix as initial name + created_at: new Date().toISOString() + } + ]); + + if (studentError) { + return res.status(400).json({ error: studentError.message }); + } + + // Insert into users table + const { data, error } = await supabase.from("users").insert([ + { + student_id, + email, + parent_email, + password: hashedPassword, + created_at: new Date().toISOString() + } + ]); + + if (error) { + // Rollback student creation if user creation fails + await supabase.from("students").delete().eq("student_id", student_id); + return res.status(400).json({ error: error.message }); + } + + res.json({ + message: "User registered successfully", + student_id, + board: INDIAN_BOARDS[board] + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Server error" }); + } +}); + +// Login with password verification +app.post("/login", async (req, res) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ error: "Email and password required" }); + } + + // Get user with student info + const { data: user, error } = await supabase + .from("users") + .select(` + *, + students ( + class, + board, + stream, + name + ) + `) + .eq("email", email) + .single(); + + if (error || !user) { + return res.status(400).json({ error: "Invalid credentials" }); + } + + // Verify password + const validPassword = await bcrypt.compare(password, user.password); + if (!validPassword) { + return res.status(400).json({ error: "Invalid credentials" }); + } + + // Create session + req.session.userId = user.id; + req.session.studentId = user.student_id; + req.session.userEmail = user.email; + req.session.studentInfo = user.students; + + // Don't send password to client + delete user.password; + + res.json({ + message: "Login successful", + data: user + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Server error" }); + } +}); + +// Logout +app.post("/logout", (req, res) => { + req.session.destroy((err) => { + if (err) { + return res.status(500).json({ error: "Could not log out" }); + } + res.json({ message: "Logged out successfully" }); + }); +}); + +// ============= GET CURRENT USER ============= +app.get("/api/me", async (req, res) => { + if (!req.session.studentId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + try { + // Fetch fresh user and student data + const { data: user, error } = await supabase + .from("users") + .select(`*, students (*)`) + .eq("student_id", req.session.studentId) + .single(); + + if (error || !user) { + return res.status(404).json({ error: "User not found" }); + } + + // Don't send the password hash + delete user.password; + + // Send all necessary info to the frontend + res.json({ + loggedInUser: user, + studentInfo: user.students + }); + + } catch (err) { + console.error(err); + res.status(500).json({ error: "Server error" }); + } +}); + +// ============= QUEST/TASK ROUTES ============= + +// Get user's quests +app.get("/quests", async (req, res) => { + try { + if (!req.session.studentId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + const { data, error } = await supabase + .from("quests") + .select("*") + .eq("student_id", req.session.studentId) + .order("created_at", { ascending: false }); + + if (error) { + return res.status(400).json({ error: error.message }); + } + + res.json(data); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Server error" }); + } +}); + +// Create quest +app.post("/quests", async (req, res) => { + try { + if (!req.session.studentId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + const { title, subject, dueDate, importance, chapter, topic } = req.body; + + const { data, error } = await supabase.from("quests").insert([ + { + student_id: req.session.studentId, + title, + subject, + due_date: dueDate, + importance, + chapter, + topic, + completed: false, + xp_value: importance === 'high' ? 50 : importance === 'medium' ? 25 : 10, + created_at: new Date().toISOString() + } + ]); + + if (error) { + return res.status(400).json({ error: error.message }); + } + + res.json({ message: "Quest created successfully", data }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Server error" }); + } +}); + +// Complete quest +app.put("/quests/:id/complete", async (req, res) => { + try { + if (!req.session.studentId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + const { id } = req.params; + + // Get quest to verify ownership and get XP value + const { data: quest, error: fetchError } = await supabase + .from("quests") + .select("*") + .eq("id", id) + .eq("student_id", req.session.studentId) + .single(); + + if (fetchError || !quest) { + return res.status(404).json({ error: "Quest not found" }); + } + + if (quest.completed) { + return res.status(400).json({ error: "Quest already completed" }); + } + + // Mark quest as completed + const { error: updateError } = await supabase + .from("quests") + .update({ + completed: true, + completed_at: new Date().toISOString() + }) + .eq("id", id); + + if (updateError) { + return res.status(400).json({ error: updateError.message }); + } + + // Update user's XP + const { data: student } = await supabase + .from("students") + .select("total_xp, level") + .eq("student_id", req.session.studentId) + .single(); + + const newXp = (student?.total_xp || 0) + quest.xp_value; + const newLevel = Math.floor(newXp / 100) + 1; + + await supabase + .from("students") + .update({ + total_xp: newXp, + level: newLevel + }) + .eq("student_id", req.session.studentId); + + res.json({ + message: "Quest completed!", + xp_earned: quest.xp_value, + total_xp: newXp, + level: newLevel + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Server error" }); + } +}); + +// deleting chat history +app.delete("/api/chat/history", async (req, res) => { + try { + if (!req.session.studentId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + const { error } = await supabase + .from("chat_history") + .delete() + .eq("student_id", req.session.studentId); + + if (error) { + return res.status(500).json({ error: "Could not delete chat history." }); + } + + res.json({ message: "Chat history cleared successfully." }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Server error" }); + } +}); + +// ============= AI CHAT ROUTES ============= + +// Enhanced AI chat with curriculum context +app.post("/api/chat", async (req, res) => { + try { + if (!req.session.studentId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + const { message, subject } = req.body; + if (!message) { + return res.status(400).json({ error: "Message is required" }); + } + + // Get student's curriculum info + const studentInfo = req.session.studentInfo || {}; + const contextPrompt = `You are Scholarly, an AI study buddy helping a ${studentInfo.board || 'Indian'} board Class ${studentInfo.class || ''} student. + ${studentInfo.stream ? `They are in the ${studentInfo.stream} stream.` : ''} + ${subject ? `The question is about ${subject}.` : ''} + Keep your answers relevant to their curriculum level and use terminology from Indian textbooks (especially NCERT when applicable). + Be encouraging, helpful, and concise.`; + + // Initialize Gemini model + const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash-latest" }); + + const chat = model.startChat({ + history: [ + { role: "user", parts: [{ text: contextPrompt }] }, + { role: "model", parts: [{ text: "Understood! I'm Scholarly, ready to help you with your studies! 📚" }] }, + ], + }); + + const result = await chat.sendMessage(message); + const response = result.response; + const text = response.text(); + + // Store chat history + await supabase.from("chat_history").insert([ + { + student_id: req.session.studentId, + message: message, + response: text, + subject: subject || null, + created_at: new Date().toISOString() + } + ]); + + res.json({ reply: text }); + + } catch (error) { + console.error("Gemini API Error:", error); + res.status(500).json({ error: "Failed to get a response from AI" }); + } +}); + +// ============= REFERENCE DATA ROUTES ============= + +// Get boards list +app.get("/api/boards", (req, res) => { + res.json(Object.keys(INDIAN_BOARDS).map(key => ({ + code: key, + name: INDIAN_BOARDS[key] + }))); +}); + +// Get classes list +app.get("/api/classes", (req, res) => { + res.json(CLASSES); +}); + +// Get subjects for a class/stream +app.get("/api/subjects/:class/:stream?", (req, res) => { + const { class: studentClass, stream } = req.params; + + let subjects = []; + if (["6", "7", "8"].includes(studentClass)) { + subjects = SUBJECTS_BY_CLASS["6-8"]; + } else if (["9", "10"].includes(studentClass)) { + subjects = SUBJECTS_BY_CLASS["9-10"]; + } else if (["11", "12"].includes(studentClass) && stream) { + subjects = SUBJECTS_BY_CLASS[`11-12-${stream}`] || []; + } + + res.json(subjects); +}); + +// Start server +const PORT = process.env.PORT || 5000; +app.listen(PORT, () => { + console.log(`🚀 Server running at http://localhost:${PORT}`); + console.log(`📚 Scholarly - Gamified Learning Platform`); + console.log(`🎮 Ready to make studying fun!`); +}); \ No newline at end of file diff --git a/index.html b/index.html index 4310d96..ec1a2e7 100644 --- a/index.html +++ b/index.html @@ -1,89 +1,216 @@ - - - - - - StudyQuest - - - -
- - -
-
-

StudyQuest

- - -
-
-

Available Quests

-
- - - - - -
-
-
-
- - -
- - - - - + + + + + + Scholarly - Quest Dashboard + + + + + + + +
+ + + + +
+
+

📚 Study Quest Board

+
+ + +
+
+ + + + +
+

Create New Quest

+
+ + + + + + + + + + + + + +
+ +

Active Quests

+
+
+ 0 + Active +
+
+ 0 + Due Today +
+
+ 0 + Overdue +
+
+ +
+ +
+
+
+ + + +
+ + + + \ No newline at end of file diff --git a/login.css b/login.css index a090176..e2c528f 100644 --- a/login.css +++ b/login.css @@ -1,140 +1,26 @@ -@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Press Start 2P', cursive; - background-color: #0f0f2e; - color: #fff; - height: 100vh; - display: flex; - align-items: center; - justify-content: center; -} - -.container { - width: 100%; - height: 100vh; - display: flex; - align-items: center; - justify-content: center; - position: relative; - background-color: #0f0f2e; -} - -#flip { - display: none; -} - -.forms { - width: 450px; - background: #1a1a3b; - padding: 40px; - border-radius: 12px; - box-shadow: 0 0 20px #5e64e0; - transition: 0.6s; - position: relative; - overflow: hidden; -} - -.form { - display: none; - flex-direction: column; -} - -#flip:not(:checked) ~ .forms .login-form { - display: flex; -} - -#flip:checked ~ .forms .signup-form { - display: flex; -} - -.title { - font-size: 1rem; - color: #b9c15d; - text-align: center; - margin-bottom: 30px; - text-shadow: 0 0 5px #7969ff; -} - -.input-box { - position: relative; - margin-bottom: 20px; -} - -.input-box input { - width: 100%; - padding: 12px 12px 12px 40px; - background: #0f0f2e; - border: 2px solid #444; - border-radius: 6px; - color: #fff; - font-size: 0.7rem; - font-family: 'Press Start 2P', cursive; -} - -.input-box i { - position: absolute; - left: 12px; - top: 50%; - transform: translateY(-50%); - color: #ff69b4; - font-size: 0.8rem; -} - -.input-box input:focus { - border-color: #ff69b4; - outline: none; - box-shadow: 0 0 10px #ff69b4; -} - -.button input { - background: #ff1493; - border: none; - padding: 12px; - border-radius: 6px; - font-size: 0.7rem; - color: white; - cursor: pointer; - transition: 0.3s; - font-family: 'Press Start 2P', cursive; -} - -.button input:hover { - background: #ff69b4; - box-shadow: 0 0 10px #ff69b4; -} - -.text { - font-size: 0.6rem; - text-align: center; - color: #fff; - margin-top: 10px; -} - -.text a, -.text label { - color: #ff69b4; - cursor: pointer; -} - -.text a:hover, -.text label:hover { - text-decoration: underline; -} -.header { - font-family: 'Press Start 2P', monospace; /* pixel style font */ - font-size: 1.8rem; - font-weight: bold; - color: white; - text-align: center; - margin-bottom: 25px; - letter-spacing: 2px; - text-shadow: 0 0 6px #ff1c80; /* pink glow effect */ - user-select: none; /* prevent text selection */ -} \ No newline at end of file +@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap'); +* { margin: 0; padding: 0; box-sizing: border-box; } +body { font-family: 'Poppins', sans-serif; background-color: #f0f2f5; color: #333; display: flex; justify-content: center; align-items: center; min-height: 100vh; } +.login-container { display: flex; width: 900px; height: 600px; background-color: #fff; border-radius: 15px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); overflow: hidden; } +.login-branding { flex: 1; background: linear-gradient(135deg, #16213e, #0f3460); color: white; display: flex; justify-content: center; align-items: center; padding: 40px; } +.branding-content { text-align: center; } +.logo { font-weight: 700; font-size: 2.5rem; margin-bottom: 15px; } +.branding-content h2 { font-family: 'Poppins', sans-serif; color: #fca311; font-size: 1.5rem; text-shadow: none; margin-bottom: 15px; } +.branding-content p { font-size: 1rem; line-height: 1.6; opacity: 0.8; } +.login-form-area { flex: 1; display: flex; justify-content: center; align-items: center; padding: 40px; } +#flip { display: none; } +.forms-wrapper { width: 100%; max-width: 350px; perspective: 1000px; position: relative; height: 480px; } +.form { position: absolute; width: 100%; backface-visibility: hidden; transition: transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1); } +.title { font-size: 1.8rem; font-weight: 600; color: #16213e; text-align: center; margin-bottom: 30px; } +.input-box { position: relative; margin-bottom: 20px; } +.input-box input, .input-box select { width: 100%; padding: 12px 12px 12px 45px; background: #f0f2f5; border: 1px solid #ddd; border-radius: 8px; font-size: 0.9rem; font-family: 'Poppins', sans-serif; } +.input-box i { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: #aaa; font-size: 1rem; } +.input-box input:focus, .input-box select:focus { border-color: #e94560; outline: none; } +.button input { background: linear-gradient(135deg, #e94560, #ff6b6b); border: none; padding: 12px; border-radius: 8px; font-size: 1rem; font-weight: 600; color: white; cursor: pointer; transition: all 0.3s; width: 100%; } +.button input:hover { transform: translateY(-2px); box-shadow: 0 4px 10px rgba(233, 69, 96, 0.3); } +.text { font-size: 0.9rem; text-align: center; color: #666; margin-top: 20px; } +.text label { color: #e94560; cursor: pointer; font-weight: 600; } +.text label:hover { text-decoration: underline; } +.signup-form { transform: rotateY(180deg); } +#flip:checked ~ .forms-wrapper .signup-form { transform: rotateY(0deg); } +#flip:checked ~ .forms-wrapper .login-form { transform: rotateY(-180deg); } \ No newline at end of file diff --git a/login.html b/login.html index e0d792d..90864e3 100644 --- a/login.html +++ b/login.html @@ -1,133 +1,90 @@ - - - - - StudyQuest | Login - - - - - -
-
scholarly
- -
- - - - - - - - - - - + + + + + Scholarly | Welcome + + + + + + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8522c25..a7b8554 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,32 @@ { - "name": "mindbenders", + "name": "scholarly", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "mindbenders", + "name": "scholarly", "version": "1.0.0", "license": "ISC", "dependencies": { + "@google/generative-ai": "^0.21.0", "@supabase/supabase-js": "^2.46.1", + "bcryptjs": "^2.4.3", "body-parser": "^1.20.3", "cors": "^2.8.5", + "dotenv": "^16.4.5", "express": "^4.21.0", - "nanoid": "^5.1.5" + "express-session": "^1.18.0", + "uuid": "^10.0.0" + } + }, + "node_modules/@google/generative-ai": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz", + "integrity": "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" } }, "node_modules/@supabase/auth-js": { @@ -133,6 +146,12 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -272,6 +291,18 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -392,6 +423,40 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -628,24 +693,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/nanoid": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", - "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -688,6 +735,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -731,6 +787,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -950,6 +1015,18 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/undici-types": { "version": "7.12.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", @@ -974,6 +1051,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/req.json b/req.json new file mode 100644 index 0000000..0b59a46 --- /dev/null +++ b/req.json @@ -0,0 +1,24 @@ +{ + "name": "scholarly", + "version": "1.0.0", + "description": "Gamified learning platform for Indian students", + "main": "server.js", + "type": "module", + "scripts": { + "start": "node server.js", + "dev": "node server.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@google/generative-ai": "^0.21.0", + "@supabase/supabase-js": "^2.46.1", + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.0", + "express-session": "^1.18.0", + "uuid": "^10.0.0" + } +} \ No newline at end of file diff --git a/resources.html b/resources.html new file mode 100644 index 0000000..82d6dd2 --- /dev/null +++ b/resources.html @@ -0,0 +1,166 @@ + + + + + + Scholarly - Study Resources + + + + + + +
+
+
+

📖 Study Resources Hub

+
+ +
+

NCERT Textbooks (CBSE)

+

Official NCERT textbooks for CBSE curriculum - Free PDFs available

+
+
+

Class 6-8

+ +
+
+

Class 9-10

+ +
+
+

Class 11-12 Science

+ +
+
+
+ +
+

Free Learning Platforms

+

Quality educational content aligned with Indian curriculum

+
+
+

🎓 DIKSHA

+

National platform for school education by Government of India

+ Visit DIKSHA +
+
+

📺 SWAYAM

+

Free online courses from Class 9 to post-graduation

+ Visit SWAYAM +
+
+

🏫 Khan Academy

+

Free courses aligned with NCERT curriculum

+ Visit Khan Academy +
+
+
+ +
+
+ + + + + + \ No newline at end of file diff --git a/scholarly-database-schema.sql b/scholarly-database-schema.sql new file mode 100644 index 0000000..a334d43 --- /dev/null +++ b/scholarly-database-schema.sql @@ -0,0 +1,80 @@ +-- Drop existing tables if they exist (for clean setup) +DROP TABLE IF EXISTS chat_history CASCADE; +DROP TABLE IF EXISTS quests CASCADE; +DROP TABLE IF EXISTS users CASCADE; +DROP TABLE IF EXISTS students CASCADE; + +-- Create students table +CREATE TABLE students ( + student_id INTEGER PRIMARY KEY, + name VARCHAR(255), + class VARCHAR(10) NOT NULL, + board VARCHAR(50) NOT NULL, + stream VARCHAR(50), + total_xp INTEGER DEFAULT 0, + level INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Create users table +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + student_id INTEGER UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + parent_email VARCHAR(255), + password VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (student_id) REFERENCES students(student_id) ON DELETE CASCADE +); + +-- Create quests table +CREATE TABLE quests ( + id SERIAL PRIMARY KEY, + student_id INTEGER NOT NULL, + title VARCHAR(255) NOT NULL, + subject VARCHAR(100), + chapter VARCHAR(100), + topic VARCHAR(255), + due_date DATE, + importance VARCHAR(20) DEFAULT 'medium', + completed BOOLEAN DEFAULT FALSE, + completed_at TIMESTAMP, + xp_value INTEGER DEFAULT 25, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (student_id) REFERENCES students(student_id) ON DELETE CASCADE +); + +-- Create chat history table +CREATE TABLE chat_history ( + id SERIAL PRIMARY KEY, + student_id INTEGER NOT NULL, + message TEXT NOT NULL, + response TEXT NOT NULL, + subject VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (student_id) REFERENCES students(student_id) ON DELETE CASCADE +); + +-- Create indexes for better performance +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_users_student_id ON users(student_id); +CREATE INDEX idx_quests_student_id ON quests(student_id); +CREATE INDEX idx_quests_completed ON quests(completed); +CREATE INDEX idx_chat_history_student_id ON chat_history(student_id); + +-- Create achievements table (optional - for future expansion) +CREATE TABLE achievements ( + id SERIAL PRIMARY KEY, + student_id INTEGER NOT NULL, + achievement_type VARCHAR(100) NOT NULL, + achievement_name VARCHAR(255) NOT NULL, + unlocked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (student_id) REFERENCES students(student_id) ON DELETE CASCADE +); + +-- Sample data for testing (optional - remove in production) +-- INSERT INTO students (student_id, name, class, board, stream) +-- VALUES (123456, 'Test Student', '10', 'CBSE', NULL); + +-- INSERT INTO users (student_id, email, parent_email, password) +-- VALUES (123456, 'test@student.com', 'parent@email.com', '$2a$10$YourHashedPasswordHere'); \ No newline at end of file diff --git a/scholarly-env.sh b/scholarly-env.sh new file mode 100644 index 0000000..b701b7c --- /dev/null +++ b/scholarly-env.sh @@ -0,0 +1,12 @@ +# Supabase Configuration +SUPABASE_URL= https://xuvbvkyhxxfrsejiaaxx.supabase.co +SUPABASE_KEY= eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh1dmJ2a3loeHhmcnNlamlhYXh4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTg1MDgwMzksImV4cCI6MjA3NDA4NDAzOX0.hz7UtYp70kC0X2GuNm4Dp1ekg8m5SuW3NRKZH0z-ZEc + +# Google Gemini AI Configuration +GEMINI_API_KEY= AIzaSyA8oupvWHHjw6_McnNJwtCLfUbM6L0ZzTw + +# Session Secret (generate a random string) +SESSION_SECRET= s3YwO7sf6y + +# Server Configuration +PORT=5000 \ No newline at end of file diff --git a/style.css b/style.css index cb052c5..ac77ca0 100644 --- a/style.css +++ b/style.css @@ -1,331 +1,968 @@ -@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Roboto:wght@400;700&display=swap'); - -/* Basic Reset & Game-like Theme */ -body { - background-color: #1a1a2e; /* Dark blue background */ - color: #e0e0e0; /* Light text color */ - font-family: 'Roboto', sans-serif; - margin: 0; - padding: 20px; - display: flex; - justify-content: center; - align-items: center; /* Changed to center for vertical centering */ - min-height: 100vh; -} - -.container { - display: flex; - gap: 20px; - width: 100%; - max-width: 1200px; - position: relative; - margin: auto; /* Added for horizontal centering within flex item */ -} - -/* Use a pixelated font for headings to give a retro game feel */ -h1, h2 { - font-family: 'Press Start 2P', cursive; - color: #fca311; /* Bright accent color */ - text-shadow: 2px 2px #000; -} - -header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; -} - -.profile-button { - background-color: #e94560; - color: white; - border: none; - padding: 10px 15px; - border-radius: 5px; - cursor: pointer; - font-family: 'Roboto', sans-serif; - font-weight: bold; - transition: background-color 0.2s; -} - -.profile-button:hover { - background-color: #fca311; -} - -/* Main Content Area (Quests) */ -main { - flex: 3; - background-color: #16213e; - padding: 20px; - border-radius: 10px; - border: 2px solid #0f3460; -} - -.quest-board h2 { - border-bottom: 2px solid #fca311; - padding-bottom: 10px; - margin-top: 0; - margin-bottom: 20px; -} - -.add-quest { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-bottom: 20px; - padding: 15px; - background-color: #0f3460; - border-radius: 8px; - border: 1px solid #e94560; -} - -.add-quest input[type="text"], -.add-quest input[type="date"], -.add-quest select { - flex: 1; - padding: 10px; - border: 1px solid #0f3460; - border-radius: 5px; - background-color: #1a1a2e; - color: #e0e0e0; - min-width: 150px; -} - -.add-quest input[type="date"]::-webkit-calendar-picker-indicator { - filter: invert(1); /* Makes the calendar icon white */ -} - -.add-quest button { - padding: 10px 15px; - background-color: #e94560; - color: white; - border: none; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.2s; -} - -.add-quest button:hover { - background-color: #fca311; -} - -.quest-list { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: 20px; -} - -.quest-card { - background-color: #0f3460; - border: 1px solid #e94560; - border-radius: 8px; - padding: 15px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); - transition: transform 0.2s, box-shadow 0.2s; -} - -.quest-card:hover { - transform: translateY(-5px); - box-shadow: 0 6px 12px rgba(233, 69, 96, 0.5); -} - -.quest-card h3 { - margin-top: 0; - color: #e94560; -} - -.quest-card p { - font-size: 0.9rem; - color: #c0c0c0; -} - -/* Player Profile Sidebar */ -aside { - flex: 1; - background-color: #16213e; - padding: 20px; - border-radius: 10px; - border: 2px solid #0f3460; - position: sticky; - top: 20px; -} - -.player-profile h2 { - text-align: center; - margin-top: 0; -} - -.player-stats { - text-align: center; - margin-bottom: 20px; -} - -.player-level { - font-size: 1.5rem; - font-weight: bold; - color: #fca311; -} - -/* XP Bar */ -.xp-bar-container { - width: 100%; - background-color: #0f3460; - border-radius: 5px; - border: 1px solid #e94560; - margin-bottom: 10px; -} - -.xp-bar { - height: 20px; - width: 0%; /* Will be updated by JS */ - background-color: #fca311; - border-radius: 4px; - transition: width 0.5s ease-in-out; -} - -.xp-text { - font-size: 0.8rem; - text-align: center; -} - -/* Achievements */ -.achievements-list { - list-style: none; - padding: 0; -} - -.achievement { - background-color: #0f3460; - margin-bottom: 10px; - padding: 10px; - border-radius: 5px; - display: flex; - align-items: center; - gap: 10px; -} - -.achievement.unlocked { - border-left: 5px solid #fca311; -} - -.achievement-icon { - font-size: 1.5rem; -} - -.achievement.unlocked .achievement-icon { - color: #fca311; -} - -/* Modal Styles */ -.modal { - display: none; /* Hidden by default */ - position: fixed; /* Stay in place */ - z-index: 1; /* Sit on top */ - left: 0; - top: 0; - width: 100%; /* Full width */ - height: 100%; /* Full height */ - overflow: auto; /* Enable scroll if needed */ - background-color: rgba(0,0,0,0.7); /* Black w/ opacity */ - display: flex; - justify-content: center; - align-items: center; -} - -.modal-content { - background-color: #16213e; - margin: auto; - padding: 30px; - border: 2px solid #0f3460; - border-radius: 10px; - width: 80%; - max-width: 500px; - box-shadow: 0 5px 15px rgba(0,0,0,0.5); - position: relative; -} - -.close-button { - color: #aaa; - position: absolute; - top: 10px; - right: 20px; - font-size: 28px; - font-weight: bold; - cursor: pointer; -} - -.close-button:hover, -.close-button:focus { - color: #e94560; - text-decoration: none; -} - -.modal-content h2 { - text-align: center; - margin-bottom: 20px; -} - -#subject-list { - margin-bottom: 20px; - max-height: 200px; - overflow-y: auto; - border: 1px solid #0f3460; - padding: 10px; - border-radius: 5px; - background-color: #1a1a2e; -} - -.subject-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 0; - border-bottom: 1px dashed #0f3460; -} - -.subject-item:last-child { - border-bottom: none; -} - -.subject-item span { - font-size: 1.1rem; -} - -.delete-subject-btn { - background-color: #e94560; - color: white; - border: none; - padding: 5px 10px; - border-radius: 3px; - cursor: pointer; - transition: background-color 0.2s; -} - -.delete-subject-btn:hover { - background-color: #fca311; -} - -#new-subject-input { - width: calc(100% - 22px); - padding: 10px; - margin-bottom: 10px; - border: 1px solid #0f3460; - border-radius: 5px; - background-color: #1a1a2e; - color: #e0e0e0; -} - -#add-subject-modal-btn { - width: 100%; - padding: 10px; - background-color: #e94560; - color: white; - border: none; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.2s; -} - -#add-subject-modal-btn:hover { - background-color: #fca311; -} +@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Roboto:wght@400;700&display=swap'); + +/* ==================== GLOBAL STYLES ==================== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + color: #e0e0e0; + font-family: 'Roboto', sans-serif; + margin: 0; + min-height: 100vh; + padding-left: 5rem; + position: relative; +} + +h1, h2 { + font-family: 'Press Start 2P', cursive; + color: #fca311; + text-shadow: 2px 2px #000; +} + +h3 { + color: #e94560; + margin-bottom: 10px; +} + +/* ==================== NAVIGATION BAR ==================== */ +.navbar { + position: fixed; + top: 0; + left: 0; + height: 100vh; + width: 5rem; + background-color: #16213e; + border-right: 2px solid #0f3460; + transition: width 200ms ease; + z-index: 100; + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.3); +} + +.navbar-nav { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + align-items: center; + height: 100%; +} + +.nav-item { + width: 100%; +} + +.nav-item:last-child { + margin-top: auto; +} + +.nav-link { + display: flex; + align-items: center; + height: 5rem; + color: #e0e0e0; + text-decoration: none; + transition: all 0.3s; +} + +.nav-link:hover { + background-color: #0f3460; + color: #fca311; +} + +.link-text { + display: none; + margin-left: 1rem; + font-size: 1rem; +} + +.nav-link i { + min-width: 2rem; + margin: 0 1.5rem; + font-size: 1.5rem; +} + +.navbar:hover { + width: 16rem; +} + +.navbar:hover .link-text { + display: inline; +} + +/* ==================== MAIN CONTAINER ==================== */ +.container { + display: flex; + gap: 20px; + width: 100%; + max-width: 1400px; + margin: 20px auto; + padding: 0 20px; +} + +/* ==================== SIDEBAR (Player Profile) ==================== */ +aside { + flex: 0 0 300px; + background-color: #16213e; + padding: 20px; + border-radius: 10px; + border: 2px solid #0f3460; + position: sticky; + top: 20px; + height: fit-content; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); +} + +.player-profile h2 { + text-align: center; + font-size: 1rem; + margin-bottom: 20px; +} + +.student-info { + background-color: #0f3460; + padding: 15px; + border-radius: 8px; + margin-bottom: 20px; + text-align: center; +} + +.student-name { + font-size: 1.2rem; + font-weight: bold; + color: #fca311; + margin-bottom: 5px; +} + +.student-board, .student-class { + font-size: 0.9rem; + color: #b0b0b0; +} + +.player-stats { + text-align: center; + margin-bottom: 20px; +} + +.player-level { + font-size: 1.8rem; + font-weight: bold; + color: #fca311; + margin-bottom: 10px; +} + +.xp-bar-container { + width: 100%; + height: 25px; + background-color: #0f3460; + border-radius: 12px; + border: 2px solid #e94560; + margin-bottom: 10px; + overflow: hidden; +} + +.xp-bar { + height: 100%; + background: linear-gradient(90deg, #fca311 0%, #ff6b6b 100%); + border-radius: 10px; + transition: width 0.5s ease-in-out; + box-shadow: 0 0 10px rgba(252, 163, 17, 0.5); +} + +.xp-text { + font-size: 0.9rem; +} + +/* Achievements */ +.achievements-list { + list-style: none; + padding: 0; +} + +.achievement { + background-color: #0f3460; + margin-bottom: 10px; + padding: 10px; + border-radius: 5px; + display: flex; + align-items: center; + gap: 10px; + opacity: 0.5; + transition: all 0.3s; +} + +.achievement.unlocked { + border-left: 5px solid #fca311; + opacity: 1; + animation: achievementUnlock 0.5s ease; +} + +@keyframes achievementUnlock { + 0% { transform: scale(0.9); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } +} + +.achievement-icon { + font-size: 1.5rem; +} + +.achievement.unlocked .achievement-icon { + color: #fca311; +} + +.stats-summary { + background-color: #0f3460; + padding: 15px; + border-radius: 8px; + margin-top: 20px; +} + +.stats-summary h3 { + font-size: 0.9rem; + color: #fca311; + margin-bottom: 10px; +} + +.stat-item { + display: flex; + justify-content: space-between; + margin-bottom: 8px; +} + +.stat-label { + font-size: 0.85rem; + color: #b0b0b0; +} + +.stat-value { + font-weight: bold; + color: #fca311; +} + +/* ==================== MAIN CONTENT ==================== */ +main { + flex: 1; + background-color: #16213e; + padding: 30px; + border-radius: 10px; + border: 2px solid #0f3460; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); +} + +header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; +} + +header h1 { + font-size: 1.5rem; +} + +.header-actions { + display: flex; + gap: 10px; +} + +.action-btn, .profile-button { + background-color: #e94560; + color: white; + border: none; + padding: 10px 15px; + border-radius: 5px; + cursor: pointer; + font-family: 'Roboto', sans-serif; + font-weight: bold; + transition: all 0.3s; +} + +.action-btn:hover, .profile-button:hover { + background-color: #fca311; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(252, 163, 17, 0.3); +} + +/* Filter Panel */ +.filter-panel { + display: flex; + gap: 10px; + margin-bottom: 20px; + padding: 15px; + background-color: #0f3460; + border-radius: 8px; + border: 1px solid #e94560; +} + +.filter-panel select { + flex: 1; + padding: 8px; + border-radius: 5px; + background-color: #1a1a2e; + color: #e0e0e0; + border: 1px solid #0f3460; +} + +/* Quest Board */ +.quest-board h2 { + border-bottom: 2px solid #fca311; + padding-bottom: 10px; + margin-bottom: 20px; +} + +.add-quest { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; + margin-bottom: 30px; + padding: 20px; + background-color: #0f3460; + border-radius: 8px; + border: 2px solid #e94560; +} + +.add-quest input[type="text"], +.add-quest input[type="date"], +.add-quest select { + padding: 12px; + border: 1px solid #0f3460; + border-radius: 5px; + background-color: #1a1a2e; + color: #e0e0e0; +} + +.add-quest input[type="date"]::-webkit-calendar-picker-indicator { + filter: invert(1); +} + +.add-quest button { + padding: 12px 20px; + background: linear-gradient(135deg, #e94560 0%, #ff6b6b 100%); + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: all 0.3s; + font-weight: bold; +} + +.add-quest button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 10px rgba(233, 69, 96, 0.4); +} + +/* Quest Statistics */ +.quest-stats { + display: flex; + gap: 15px; + margin-bottom: 20px; +} + +.stat-card { + flex: 1; + background-color: #0f3460; + padding: 15px; + border-radius: 8px; + text-align: center; + border: 1px solid #e94560; +} + +.stat-number { + font-size: 2rem; + font-weight: bold; + color: #fca311; + display: block; +} + +.stat-card .stat-label { + font-size: 0.9rem; + color: #b0b0b0; + margin-top: 5px; +} + +/* Quest List */ +.quest-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 20px; +} + +.quest-card { + background: linear-gradient(135deg, #0f3460 0%, #1a1a2e 100%); + border: 2px solid #e94560; + border-radius: 10px; + padding: 20px; + transition: all 0.3s; + position: relative; + overflow: hidden; +} + +.quest-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 4px; + background: linear-gradient(90deg, #fca311, #e94560); +} + +.quest-card.importance-high::before { + background: #ff4757; + height: 6px; +} + +.quest-card.importance-medium::before { + background: #fca311; +} + +.quest-card.importance-low::before { + background: #4cd137; +} + +.quest-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 20px rgba(233, 69, 96, 0.3); +} + +.quest-card.completed { + opacity: 0.7; + background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); +} + +.quest-header { + display: flex; + justify-content: space-between; + align-items: start; + margin-bottom: 15px; +} + +.quest-card h3 { + color: #e94560; + font-size: 1.1rem; + margin: 0; + flex: 1; +} + +.xp-badge { + background: linear-gradient(135deg, #fca311, #ff6b6b); + color: white; + padding: 5px 10px; + border-radius: 15px; + font-size: 0.8rem; + font-weight: bold; +} + +.quest-details p { + font-size: 0.9rem; + color: #c0c0c0; + margin: 8px 0; +} + +.quest-details i { + margin-right: 8px; + color: #fca311; +} + +.due-date.overdue { + color: #ff4757 !important; + font-weight: bold; +} + +.due-date.due-today { + color: #fca311 !important; + font-weight: bold; +} + +.due-date.due-soon { + color: #f39c12 !important; +} + +.quest-actions { + display: flex; + gap: 10px; + margin-top: 15px; +} + +.complete-quest-btn, .delete-quest-btn { + flex: 1; + padding: 10px; + border: none; + border-radius: 5px; + cursor: pointer; + transition: all 0.3s; + font-weight: bold; +} + +.complete-quest-btn { + background: linear-gradient(135deg, #4cd137, #00d2d3); + color: white; +} + +.complete-quest-btn:hover { + transform: scale(1.05); + box-shadow: 0 4px 10px rgba(76, 209, 55, 0.4); +} + +.delete-quest-btn { + background-color: #2c3e50; + color: #e74c3c; + flex: 0 0 auto; +} + +.delete-quest-btn:hover { + background-color: #e74c3c; + color: white; +} + +.completed-badge { + background: linear-gradient(135deg, #27ae60, #2ecc71); + color: white; + padding: 10px; + border-radius: 5px; + text-align: center; + flex: 1; +} + +.no-quests { + text-align: center; + padding: 40px; + color: #b0b0b0; + font-size: 1.1rem; +} + +/* ==================== MODAL STYLES ==================== */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + align-items: center; + justify-content: center; +} + +.modal-content { + background-color: #16213e; + padding: 30px; + border: 2px solid #0f3460; + border-radius: 10px; + width: 90%; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; + position: relative; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); +} + +.close-button { + color: #aaa; + position: absolute; + top: 10px; + right: 20px; + font-size: 28px; + font-weight: bold; + cursor: pointer; + transition: color 0.3s; +} + +.close-button:hover { + color: #e94560; +} + +.profile-section { + margin-bottom: 30px; + padding-bottom: 20px; + border-bottom: 1px solid #0f3460; +} + +.profile-section:last-child { + border-bottom: none; +} + +.profile-info p { + margin: 10px 0; + font-size: 1rem; +} + +.profile-info strong { + color: #fca311; + margin-right: 10px; +} + +#subject-list { + max-height: 200px; + overflow-y: auto; + border: 1px solid #0f3460; + padding: 10px; + border-radius: 5px; + background-color: #1a1a2e; + margin-bottom: 15px; +} + +.subject-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px; + border-bottom: 1px dashed #0f3460; +} + +.subject-item:last-child { + border-bottom: none; +} + +.delete-subject-btn { + background-color: #e94560; + color: white; + border: none; + padding: 5px 10px; + border-radius: 3px; + cursor: pointer; + transition: background-color 0.3s; +} + +.delete-subject-btn:hover { + background-color: #ff4757; +} + +.add-subject-form { + display: flex; + gap: 10px; +} + +#new-subject-input { + flex: 1; + padding: 10px; + border: 1px solid #0f3460; + border-radius: 5px; + background-color: #1a1a2e; + color: #e0e0e0; +} + +#add-subject-modal-btn { + padding: 10px 20px; + background-color: #e94560; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +#add-subject-modal-btn:hover { + background-color: #fca311; +} + +.checkbox-label { + display: block; + margin: 10px 0; + cursor: pointer; +} + +.checkbox-label input { + margin-right: 10px; +} + +/* ==================== NOTIFICATIONS ==================== */ +.notification { + position: fixed; + top: 20px; + right: 20px; + padding: 15px 20px; + border-radius: 8px; + background-color: #16213e; + border: 2px solid #0f3460; + color: white; + z-index: 2000; + animation: slideIn 0.3s ease; +} + +.notification.success { + border-color: #4cd137; + background: linear-gradient(135deg, #27ae60, #2ecc71); +} + +.notification.xp { + border-color: #fca311; + background: linear-gradient(135deg, #f39c12, #fca311); + font-weight: bold; +} + +.notification.info { + border-color: #3498db; +} + +.notification.fade-out { + animation: slideOut 0.3s ease; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } +} + +.level-up-notification { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: linear-gradient(135deg, #fca311, #ff6b6b); + padding: 30px 50px; + border-radius: 15px; + text-align: center; + z-index: 3000; + animation: levelUpPop 0.5s ease; +} + +.level-up-notification h2 { + color: white; + font-size: 2rem; + margin-bottom: 10px; +} + +.level-up-notification p { + color: white; + font-size: 1.2rem; +} + +@keyframes levelUpPop { + 0% { + transform: translate(-50%, -50%) scale(0); + } + 50% { + transform: translate(-50%, -50%) scale(1.2); + } + 100% { + transform: translate(-50%, -50%) scale(1); + } +} + +/* ==================== CHAT PAGE STYLES ==================== */ +.chat-container { + width: 100%; + max-width: 900px; + margin: 20px auto; + display: flex; + flex-direction: column; + height: calc(100vh - 40px); + background-color: #16213e; + padding: 20px; + border-radius: 10px; + border: 2px solid #0f3460; +} + +.chat-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 2px solid #fca311; +} + +.subject-selector { + display: flex; + gap: 10px; + align-items: center; +} + +.subject-selector select { + padding: 8px 12px; + border-radius: 5px; + background-color: #0f3460; + color: #e0e0e0; + border: 1px solid #e94560; +} + +.chat-window { + flex-grow: 1; + overflow-y: auto; + padding: 20px; + background-color: #0f3460; + border-radius: 8px; + margin-bottom: 20px; +} + +.message { + margin-bottom: 15px; + padding: 12px 16px; + border-radius: 10px; + max-width: 70%; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.user-message { + background: linear-gradient(135deg, #e94560, #ff6b6b); + color: white; + margin-left: auto; + text-align: right; +} + +.bot-message { + background-color: #1a1a2e; + border: 1px solid #e94560; + margin-right: auto; +} + +.bot-message p { + margin: 0; + line-height: 1.5; +} + +.typing-indicator { + display: inline-block; + padding: 12px 16px; + background-color: #1a1a2e; + border: 1px solid #e94560; + border-radius: 10px; +} + +.typing-indicator span { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #fca311; + margin: 0 2px; + animation: typing 1.4s infinite; +} + +.typing-indicator span:nth-child(2) { + animation-delay: 0.2s; +} + +.typing-indicator span:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes typing { + 0%, 60%, 100% { + transform: translateY(0); + } + 30% { + transform: translateY(-10px); + } +} + +.chat-input-area { + display: flex; + gap: 10px; +} + +#chat-input { + flex-grow: 1; + padding: 12px; + border-radius: 8px; + background-color: #1a1a2e; + color: #e0e0e0; + border: 1px solid #0f3460; + resize: none; + font-family: 'Roboto', sans-serif; +} + +#send-btn { + padding: 12px 20px; + background: linear-gradient(135deg, #e94560, #ff6b6b); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 1.2rem; + transition: all 0.3s; +} + +#send-btn:hover { + transform: scale(1.05); + box-shadow: 0 4px 10px rgba(233, 69, 96, 0.4); +} + +/* ==================== RESPONSIVE DESIGN ==================== */ +@media (max-width: 768px) { + body { + padding-left: 0; + padding-top: 60px; + } + + .navbar { + width: 100%; + height: 60px; + border-right: none; + border-bottom: 2px solid #0f3460; + } + + .navbar-nav { + flex-direction: row; + justify-content: space-around; + } + + .nav-item:last-child { + margin-top: 0; + } + + .nav-link { + height: 60px; + } + + .navbar:hover { + width: 100%; + } + + .link-text { + display: none !important; + } + + .container { + flex-direction: column; + } + + aside { + flex: none; + width: 100%; + position: static; + } + + .quest-list { + grid-template-columns: 1fr; + } + + .add-quest { + grid-template-columns: 1fr; + } +} \ No newline at end of file