diff --git a/app/LogIn/page.js b/app/LogIn/page.js index 346c7ac..5519344 100644 --- a/app/LogIn/page.js +++ b/app/LogIn/page.js @@ -10,8 +10,11 @@ import { } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import axios from "axios"; +import { Alert } from "@mui/material"; +import api from "@/utils/api"; +import { useAuth } from "@/contexts/AuthContext"; import React, { useState } from "react"; +import { useRouter } from "next/navigation"; import Link from "next/link"; import { Alert, Snackbar } from "@mui/material"; import { useRouter } from "next/navigation"; @@ -19,11 +22,13 @@ import { useRouter } from "next/navigation"; function LogIn() { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); - const [alertOpen, setAlertOpen] = useState(false); - const [alertMessage, setAlertMessage] = useState(""); - const [alertSeverity, setAlertSeverity] = useState("success"); - const navigate = useRouter(); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const router = useRouter(); + const { checkAuthStatus } = useAuth(); + const handleUsernameChange = (event) => { setUsername(event.target.value); @@ -36,66 +41,39 @@ function LogIn() { const handleLogin = async (event) => { event.preventDefault(); - if (!username || !password) { - setAlertMessage("Please enter both username and password"); - setAlertSeverity("error"); - setAlertOpen(true); - return; - } + setLoading(true); + setError(""); try { - const response = await axios.post( - `http://localhost:8080/api/signup/login`, - { - username, - password, - }, - { - withCredentials: true, - } - ); + const response = await api.post("/auth/login", { + username, + password, + }); - console.log(response.data); + console.log("Login successful:", response.data); - if (response.data.message === "Login successful") { - setAlertMessage("Log In successfully! Redirecting..."); - setAlertSeverity("success"); - setAlertOpen(true); + // Update auth context + await checkAuthStatus(); - setTimeout(() => { - navigate.push("/DashBoard"); - }, 1500); - } else { - throw new Error("Login failed - unexpected response"); - } + // Redirect to dashboard + router.push("/"); } catch (error) { - console.log("error", error.response); - - let errorMessage = "Log In failed"; - - if (error.response) { - if (error.response.status === 401) { - errorMessage = "Invalid username or password"; - } else if (error.response.status === 400) { - errorMessage = error.response.data.error || "Invalid input"; - } else if (error.response.status === 500) { - errorMessage = "Server error. Please try again later."; - } else { - errorMessage = error.response.data.error || "Log In failed"; - } - } else if (error.request) { - errorMessage = - "Unable to connect to server. Please check your connection."; - } else { - errorMessage = error.message || "An unexpected error occurred"; - } - - setAlertMessage(errorMessage); - setAlertSeverity("error"); - setAlertOpen(true); + console.error("Login error:", error); + setError( + error.response?.data?.error || "Login failed. Please try again." + ); + } finally { + setLoading(false); + + } }; + const handleGoogleLogin = () => { + // Redirect to backend Google OAuth + window.location.href = `http://localhost:8080/auth/google`; + }; + return ( <> - - - - - + + + {error && {error}} + + + + + + + ); } export default LogIn; diff --git a/app/SignUp/page.js b/app/SignUp/page.js index 8002694..f6d1947 100644 --- a/app/SignUp/page.js +++ b/app/SignUp/page.js @@ -13,10 +13,14 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import React, { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; import zxcvbn from "zxcvbn"; import { validate } from "email-validator"; -import { Alert, Snackbar } from "@mui/material"; -import axios from "axios"; + +import { Alert } from "@mui/material"; +import api from "@/utils/api"; +import { useAuth } from "@/contexts/AuthContext"; + import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -27,6 +31,8 @@ export default function Signup() { const [email, setEmail] = useState(""); const [username, setUsername] = useState(""); const [validation, setValidation] = useState(false); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); const [passwordErrors, setPasswordErrors] = useState([]); const [passwordStrength, setPasswordStrength] = useState(null); const [alertOpen, setAlertOpen] = useState(false); @@ -35,6 +41,9 @@ export default function Signup() { const navigate = useRouter(); + const router = useRouter(); + const { checkAuthStatus } = useAuth(); + const handleFirstNameChange = (event) => { setFirstName(event.target.value); }; @@ -87,98 +96,67 @@ export default function Signup() { const handleSignUp = async (event) => { event.preventDefault(); - if (!firstName || !lastName || !username || !email || !password) { - setAlertMessage("Please fill in all fields"); - setAlertSeverity("error"); - setAlertOpen(true); - return; - } + setLoading(true); + setError(""); + setValidation(false); + // Validate email if (!validate(email)) { - setAlertMessage("Please enter a valid email address"); - setAlertSeverity("error"); - setAlertOpen(true); + setError("Please enter a valid email address"); + setLoading(false); return; } - if (password.length < 8) { - setAlertMessage("Password must be at least 8 characters long"); - setAlertSeverity("error"); - setAlertOpen(true); + // Validate password strength + if (passwordErrors.length > 0 || passwordStrength?.score < 2) { + setError("Please choose a stronger password"); + setLoading(false); return; } - if (passwordStrength && passwordStrength.score < 2) { - setAlertMessage( - "Password is too weak. Please choose a stronger password" - ); - setAlertSeverity("error"); - setAlertOpen(true); - return; - } + console.log("๐Ÿ“ Starting signup process..."); + console.log("๐Ÿ” Signup data:", { + firstName, + lastName, + username, + email, + password: "***", + }); try { - const res = await axios.post( - `http://localhost:8080/api/signup`, - { - firstName, - lastName, - username, - email, - password, - }, - { - withCredentials: true, - } - ); - - if (res.data.message === "User created successfully") { - setAlertMessage("Signed up successfully! Redirecting..."); - setAlertSeverity("success"); - setAlertOpen(true); - - // Clear form - setFirstName(""); - setLastName(""); - setUsername(""); - setEmail(""); - setPassword(""); - setPasswordStrength(null); - setPasswordErrors([]); - - // Redirect after 2 seconds - setTimeout(() => { - navigate.push("/LogIn"); - }, 1500); - } else { - throw new Error("Signup failed - unexpected response"); - } + console.log("๐ŸŒ Making signup request to backend..."); + const response = await api.post("/auth/signup", { + firstName, + lastName, + username, + email, + password, + }); + + + console.log("โœ… Signup successful:", response.data); + setValidation(true); + + // Update auth context + await checkAuthStatus(); + + // Redirect to dashboard after a brief delay to show success message + setTimeout(() => { + router.push("/"); + }, 1500); } catch (error) { - console.error(error); - - let errorMessage = "Sign-up failed"; - - if (error.response) { - if (error.response.status === 409) { - errorMessage = - "Username or email already exists. Please choose different credentials."; - } else if (error.response.status === 400) { - errorMessage = error.response.data.error || "Invalid input data"; - } else if (error.response.status === 500) { - errorMessage = "Server error. Please try again later."; - } else { - errorMessage = error.response.data.error || "Sign-up failed"; - } - } else if (error.request) { - errorMessage = - "Unable to connect to server. Please check your connection."; - } else { - errorMessage = error.message || "An unexpected error occurred"; - } - - setAlertMessage(errorMessage); - setAlertSeverity("error"); - setAlertOpen(true); + console.error("โŒ Signup error:", error); + console.error("๐Ÿ” Error details:", { + message: error.message, + status: error.response?.status, + data: error.response?.data, + config: error.config, + }); + setError( + error.response?.data?.error || "Signup failed. Please try again." + ); + } finally { + setLoading(false); } }; @@ -283,34 +261,59 @@ export default function Signup() { value={password} /> - {passwordStrength ? ( -
-
{passwordStrengthBar(passwordStrength)}
- {passwordStrength.feedback.suggestions.length ? ( -

- Feedback:{" "} - {passwordStrength.feedback.suggestions.join(", ")} -

- ) : null} -
- ) : null} - - {/* */} + + - - - - - + {passwordStrength ? ( +
+
{passwordStrengthBar(passwordStrength)}
+ {passwordStrength.feedback.suggestions.length ? ( +

+ Feedback:{" "} + {passwordStrength.feedback.suggestions.join(", ")} +

+ ) : null} +
+ ) : null} + + {error && {error}} + {validation && ( + + Signed up successfully! Redirecting... + + )} + + + + + + + ); } diff --git a/app/StudySession/page.js b/app/StudySession/page.js index 5a87d41..375fc91 100644 --- a/app/StudySession/page.js +++ b/app/StudySession/page.js @@ -167,7 +167,7 @@ function StudyTimer() {
{ - try { - const res = await axios.get(`http://localhost:8080/api/tasks/${userId}`); - setTasks(res.data); - } catch (error) { - console.error("Error fetching tasks:", error); - } - }; - - useEffect(() => { - fetchTasks(); - }, [userId]); - - const handleDelete = async (taskId) => { - try { - await axios.delete(`http://localhost:8080/api/tasks/${userId}/${taskId}`); - fetchTasks(); - } catch (error) { - console.error("Error deleting task:", error); - } - }; - - const handleAddTask = async () => { - try { - const res = await axios.post(`http://localhost:8080/api/tasks/${userId}`, newTask); - setTasks((prev) => [...prev, res.data]); - setShowNewRow(false); - setNewTask({ - className: "", - assignment: "", - description: "", - status: "pending", - deadline: "", - priority: "medium", - }); - } catch (error) { - console.error("Error adding task:", error); - } - }; - -const handleEditChange = (e) => { - const { name, value } = e.target; - setEditTask({ ...editTask, [name]: value }); - }; - - const handleEditTask = async () => { - try { - await axios.put(`http://localhost:8080/api/tasks/${userId}/${editTask.id}`, editTask); - setEditTask(null); - fetchTasks(); - } catch (error) { - console.error("Error editing task:", error); - } - }; - -// const handleStatusChange = async (taskId, newStatus) => { -// try { -// await axios.patch(`http://localhost:8080/api/tasks/${userId}/${taskId}`, { status: newStatus }); -// fetchTasks(); // Refresh the list -// } catch (error) { -// console.error("Error updating status:", error); -// } -// }; - - // Filtering logic - const filteredTasks = tasks.filter(task => - task.className.toLowerCase().includes(filterClassName.toLowerCase()) && - (filterStatus ? task.status === filterStatus : true) && - (filterPriority ? task.priority === filterPriority : true) - ); - - return ( -
-

Your Tasks (User {userId})

- {/* Filter UI */} -
- setFilterClassName(e.target.value)} - className="max-w-xs" - /> - - - - - - setFilterStatus("")}>All - setFilterStatus("pending")}>Pending - setFilterStatus("in-progress")}>In-Progress - setFilterStatus("completed")}>Completed - - - - - - - - setFilterPriority("")}>All - setFilterPriority("low")}>Low - setFilterPriority("medium")}>Medium - setFilterPriority("high")}>High - - -
- {filteredTasks.length === 0 ? ( -

No tasks found.

- ) : ( - - - - - - - - - - - - - - {filteredTasks.map((task) => ( - editTask && editTask.id === task.id ? ( - - - - - - - - - - ) : ( - - - - - - - - - - ) - ))} - {showNewRow && ( - - - - - - - - - - )} - -
Class NameAssignmentDescriptionStatusDeadlinePriorityActions
- - - - - - setEditTask({ ...editTask, status: "pending" })}>Pending - setEditTask({ ...editTask, status: "in-progress" })}>In-Progress - setEditTask({ ...editTask, status: "completed" })}>Completed - - - - - - - - - { - setEditTask({ ...editTask, deadline: date ? date.toISOString().split("T")[0] : "" }); - setEditCalendarOpen(false); - }} - /> - - - - - - - - - setEditTask({ ...editTask, priority: "low" })}>Low - setEditTask({ ...editTask, priority: "medium" })}>Medium - setEditTask({ ...editTask, priority: "high" })}>High - - - - - -
{task.className}{task.assignment}{task.description}{task.status}{task.deadline ? new Date(task.deadline).toLocaleDateString() : ""}{task.priority} - - -
- setNewTask({ ...newTask, className: e.target.value })} - placeholder="Class Name" - /> - - setNewTask({ ...newTask, assignment: e.target.value })} - placeholder="Assignment" - /> - - setNewTask({ ...newTask, description: e.target.value })} - placeholder="Description" - /> - - - - - - - setNewTask({ ...newTask, status: "pending" })}>Pending - setNewTask({ ...newTask, status: "in-progress" })}>In-Progress - setNewTask({ ...newTask, status: "completed" })}>Completed - - - - - - - - - { - setNewTask({ ...newTask, deadline: date ? date.toISOString().split("T")[0] : "" }); - setCalendarOpen(false); - }} - /> - - - - - - - - - setNewTask({ ...newTask, priority: "low" })}>Low - setNewTask({ ...newTask, priority: "medium" })}>Medium - setNewTask({ ...newTask, priority: "high" })}>High - - - - - -
- )} - {!showNewRow && ( - - )} -
- ); -} - diff --git a/app/Tasks/page.jsx b/app/Tasks/page.jsx new file mode 100644 index 0000000..431a73b --- /dev/null +++ b/app/Tasks/page.jsx @@ -0,0 +1,592 @@ +"use client"; +import { useEffect, useState } from "react"; +import { useAuth } from "@/contexts/AuthContext"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Calendar } from "@/components/ui/calender"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ChevronDownIcon } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem +} from "@/components/ui/dropdown-menu"; +import api from "@/utils/api"; +import CalendarService from "@/utils/calendarService"; +import { CalendarNotification } from "@/components/CalendarNotification"; +import "./tasks.css"; + +export default function TasksPage() { + const { user, loading } = useAuth(); + const router = useRouter(); + const searchParams = useSearchParams(); + const [tasks, setTasks] = useState([]); + const [tasksLoading, setTasksLoading] = useState(true); + const [error, setError] = useState(""); + const [showNewRow, setShowNewRow] = useState(false); + const [editTask, setEditTask] = useState(null); + // Calendar integration state + const [calendarEnabled, setCalendarEnabled] = useState(false); + const [calendarPermissions, setCalendarPermissions] = useState(false); + const [showCalendarNotification, setShowCalendarNotification] = useState(true); + + const [newTask, setNewTask] = useState({ + className: "", + assignment: "", + description: "", + status: "pending", // Match ENUM exactly + deadline: "", + priority: "medium", + // Add calendar reminder option + createReminder: true, + }); + // Filter states + const [filterClassName, setFilterClassName] = useState(""); + const [filterStatus, setFilterStatus] = useState(""); + const [filterPriority, setFilterPriority] = useState(""); + + const [calendarOpen, setCalendarOpen] = useState(false); + const [editCalendarOpen, setEditCalendarOpen] = useState(false); + + // Redirect to login if not authenticated + useEffect(() => { + if (!loading && !user) { + router.push('/LogIn'); + } + }, [user, loading, router]); + + // Fetch tasks when user is authenticated + useEffect(() => { + if (user) { + fetchTasks(); + checkCalendarPermissions(); + } + }, [user]); + + // Handle calendar success parameter from OAuth redirect + useEffect(() => { + const calendarSuccess = searchParams.get('calendar_success'); + + if (calendarSuccess === 'permissions_granted') { + // Show success message + alert('โœ… Google Calendar connected successfully! You can now create calendar reminders for your tasks.'); + + // Clear the URL parameter + const url = new URL(window.location); + url.searchParams.delete('calendar_success'); + window.history.replaceState({}, document.title, url.pathname); + + // Refresh calendar permissions + checkCalendarPermissions(); + } + }, [searchParams]); + + // Check if user has granted calendar permissions + const checkCalendarPermissions = async () => { + try { + const hasPermissions = await CalendarService.checkCalendarPermissions(); + setCalendarPermissions(hasPermissions); + setCalendarEnabled(hasPermissions); + } catch (error) { + console.log("Calendar permissions check failed:", error); + } + }; + + const fetchTasks = async () => { + try { + setTasksLoading(true); + const response = await api.get('/api/tasks'); + setTasks(response.data); + } catch (error) { + console.error("Error fetching tasks:", error); + setError('Failed to load tasks'); + } finally { + setTasksLoading(false); + } + }; + + const handleDelete = async (taskId) => { + try { + const taskToDelete = tasks.find(task => task.id === taskId); + + // Delete calendar reminder if it exists and user has permissions + if (calendarEnabled && calendarPermissions && taskToDelete?.calendarEventId) { + try { + await CalendarService.deleteTaskReminder(taskToDelete); + console.log("Calendar reminder deleted"); + } catch (calendarError) { + console.error("Failed to delete calendar reminder:", calendarError); + } + } + + await api.delete(`/api/tasks/${taskId}`); + setTasks(tasks.filter(task => task.id !== taskId)); + } catch (error) { + console.error("Error deleting task:", error); + setError('Failed to delete task'); + } + }; + + const handleAddTask = async () => { + try { + const response = await api.post('/api/tasks', newTask); + const createdTask = response.data; + + // Create calendar reminder if enabled, permissions granted, and deadline is set + if (calendarEnabled && + calendarPermissions && + newTask.createReminder && + newTask.deadline && + newTask.deadline.trim() !== "" && + createdTask.deadline && + createdTask.deadline.trim() !== "") { + try { + console.log('Creating calendar reminder for task:', createdTask); + const calendarEvent = await CalendarService.createTaskReminder(createdTask); + console.log("Calendar reminder created:", calendarEvent); + } catch (calendarError) { + console.error("Failed to create calendar reminder:", calendarError); + + // Show detailed error for debugging + const errorDetails = calendarError.response ? + `Status: ${calendarError.response.status}, Message: ${calendarError.response.data?.error || calendarError.message}` : + calendarError.message; + + // Show user-friendly message for calendar errors + if (calendarError.message.includes("temporarily unavailable")) { + setError(`Task created successfully, but calendar reminder failed. Backend Error: ${errorDetails}`); + } else if (calendarError.message.includes("not available")) { + console.log("Calendar API not available - continuing without reminder"); + } else { + setError(`Task created successfully, but calendar reminder failed. Error: ${errorDetails}`); + } + + // Clear error after 10 seconds for debugging + setTimeout(() => setError(""), 10000); + } + } + + setTasks([...tasks, createdTask]); + setShowNewRow(false); + setNewTask({ + className: "", + assignment: "", + description: "", + status: "pending", + deadline: "", + priority: "medium", + createReminder: true, + }); + } catch (error) { + console.error("Error adding task:", error); + setError(`Failed to create task: ${error.response?.data?.error || error.message}`); + } + }; + + const handleEditChange = (e) => { + const { name, value } = e.target; + setEditTask({ ...editTask, [name]: value }); + }; + + const handleEditTask = async () => { + try { + const response = await api.put(`/api/tasks/${editTask.id}`, editTask); + const updatedTask = response.data; + + // Update calendar reminder if task has one, deadline changed, and user has permissions + if (calendarEnabled && calendarPermissions && updatedTask.calendarEventId && editTask.deadline) { + try { + await CalendarService.updateTaskReminder(updatedTask); + console.log("Calendar reminder updated"); + } catch (calendarError) { + console.error("Failed to update calendar reminder:", calendarError); + } + } + + setTasks(tasks.map(task => task.id === editTask.id ? updatedTask : task)); + setEditTask(null); + } catch (error) { + console.error("Error editing task:", error); + setError('Failed to update task'); + } + }; + + // Authentication loading + if (loading) { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + // Not authenticated + if (!user) { + return null; // Will redirect to login + } + + // Filtering logic + const filteredTasks = tasks.filter(task => + task.className.toLowerCase().includes(filterClassName.toLowerCase()) && + (filterStatus ? task.status === filterStatus : true) && + (filterPriority ? task.priority === filterPriority : true) + ); + + return ( +
+

+ Assignment Tracker +

+ +
+

Welcome back, {user.username}!

+ + {/* Calendar Integration Status */} +
+ {calendarPermissions ? ( +
+ + ๐Ÿ“… Google Calendar Connected + + +
+ ) : ( + + )} +
+
+ + {error && ( +
+ {error} + +
+ )} + + {/* Calendar Integration Notification */} + {showCalendarNotification && !calendarPermissions && ( + setShowCalendarNotification(false)} /> + )} + + {/* Filter UI */} +
+ setFilterClassName(e.target.value)} + className="max-w-xs" + /> + + + + + + setFilterStatus("")}>All + setFilterStatus("pending")}>Pending + setFilterStatus("in-progress")}>In Progress + setFilterStatus("completed")}>Completed + + + + + + + + setFilterPriority("")}>All + setFilterPriority("low")}>Low + setFilterPriority("medium")}>Medium + setFilterPriority("high")}>High + + +
+ + {/* Tasks Loading */} + {tasksLoading ? ( +
+
+

Loading tasks...

+
+ ) : ( + <> + {filteredTasks.length === 0 && !showNewRow ? ( +
+

No tasks found. Create your first task!

+
+ ) : ( + + + + + + + + + + + + + + {filteredTasks.map((task) => ( + editTask && editTask.id === task.id ? ( + + + + + + + + + + ) : ( + + + + + + + + + + ) + ))} + {showNewRow && ( + + + + + + + + + + )} + +
Class NameAssignmentDescriptionStatusDeadlinePriorityActions
+ + + + + + setEditTask({ ...editTask, status: "pending" })}>Pending + setEditTask({ ...editTask, status: "in-progress" })}>In Progress + setEditTask({ ...editTask, status: "completed" })}>Completed + + + + + + + + + { + // Set deadline to end of day (11:59 PM) to make it clear this is a full-day deadline + const deadlineWithTime = date ? + new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59).toISOString() : + ""; + setEditTask({ ...editTask, deadline: deadlineWithTime }); + setEditCalendarOpen(false); + }} + /> + + + + + + + + + setEditTask({ ...editTask, priority: "low" })}>Low + setEditTask({ ...editTask, priority: "medium" })}>Medium + setEditTask({ ...editTask, priority: "high" })}>High + + + + + +
{task.className}{task.assignment}{task.description} + + {task.status} + + +
+ {task.deadline ? new Date(task.deadline).toLocaleDateString() : "No deadline"} + {task.calendarEventId && ( + ๐Ÿ“… + )} +
+
+ + {task.priority} + + + + +
+ setNewTask({ ...newTask, className: e.target.value })} + placeholder="Class Name" + /> + + setNewTask({ ...newTask, assignment: e.target.value })} + placeholder="Assignment" + /> + + setNewTask({ ...newTask, description: e.target.value })} + placeholder="Description" + /> + + + + + + + setNewTask({ ...newTask, status: "pending" })}>Pending + setNewTask({ ...newTask, status: "in-progress" })}>In Progress + setNewTask({ ...newTask, status: "completed" })}>Completed + + + + + + + + + { + // Set deadline to end of day (11:59 PM) to make it clear this is a full-day deadline + const deadlineWithTime = date ? + new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59).toISOString() : + ""; + setNewTask({ ...newTask, deadline: deadlineWithTime }); + setCalendarOpen(false); + }} + /> + + + + + + + + + setNewTask({ ...newTask, priority: "low" })}>Low + setNewTask({ ...newTask, priority: "medium" })}>Medium + setNewTask({ ...newTask, priority: "high" })}>High + + + +
+ {calendarEnabled && newTask.deadline && ( + + )} +
+ + +
+
+
+ )} + + {!showNewRow && ( + + )} + + )} +
+ ); +} + diff --git a/app/auth/callback/page.js b/app/auth/callback/page.js new file mode 100644 index 0000000..52f74ea --- /dev/null +++ b/app/auth/callback/page.js @@ -0,0 +1,45 @@ +"use client"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/contexts/AuthContext"; + +export default function AuthCallback() { + const router = useRouter(); + const { checkAuthStatus } = useAuth(); + + useEffect(() => { + const handleAuthCallback = async () => { + // Check if this is a calendar OAuth callback + const urlParams = new URLSearchParams(window.location.search); + const isCalendarCallback = + urlParams.get("calendar") === "success" || + window.location.href.includes("calendar"); + + // Give the backend time to set the JWT cookie + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Check authentication status + await checkAuthStatus(); + + // Redirect based on callback type + if (isCalendarCallback) { + // Calendar OAuth callback - redirect to Tasks with success parameter + router.push("/Tasks?calendar_success=permissions_granted"); + } else { + // Regular login callback - redirect to home page + router.push("/"); + } + }; + + handleAuthCallback(); + }, [checkAuthStatus, router]); + + return ( +
+
+
+

Completing authentication...

+
+
+ ); +} diff --git a/app/dashboard/page.js b/app/dashboard/page.js new file mode 100644 index 0000000..e69de29 diff --git a/app/layout.js b/app/layout.js index 4bb2ab6..855a090 100644 --- a/app/layout.js +++ b/app/layout.js @@ -1,6 +1,7 @@ import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider"; +import { AuthProvider } from "@/contexts/AuthContext"; import { Poppins } from "next/font/google"; import { Rubik_Glitch_Pop } from "next/font/google"; @@ -40,15 +41,19 @@ export default function RootLayout({ children }) { <> - - - {children} - + + + + + {children} + + + diff --git a/components/CalendarNotification.jsx b/components/CalendarNotification.jsx new file mode 100644 index 0000000..7cba126 --- /dev/null +++ b/components/CalendarNotification.jsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { Button } from '@/components/ui/button'; + +export function CalendarNotification({ onDismiss }) { + const [dismissed, setDismissed] = useState(false); + + if (dismissed) return null; + + const handleDismiss = () => { + setDismissed(true); + onDismiss?.(); + }; + + return ( +
+
+
+
๐Ÿ“…
+
+

+ New! Google Calendar Integration +

+

+ Automatically create calendar reminders for your assignment deadlines. + Get email and popup notifications to never miss a due date! +

+
+ โœ… Email reminders (1 day + 10 minutes before) +
+ โœ… Popup notifications (1 hour before) +
+ โœ… Color-coded by priority +
+
+
+ +
+
+ ); +} diff --git a/components/NavBar.jsx b/components/NavBar.jsx index 65ef63e..75da263 100644 --- a/components/NavBar.jsx +++ b/components/NavBar.jsx @@ -1,3 +1,4 @@ +"use client"; import { Button } from "@/components/ui/button"; import { NavigationMenu, @@ -10,8 +11,11 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import { useAuth } from "@/contexts/AuthContext"; +import { useRouter } from "next/navigation"; import ThemeToggle from "./ThemeToggle"; + // Navigation links array to be used in both desktop and mobile menus const navigationLinks = [ { href: "#", label: "Home", active: true }, @@ -20,7 +24,31 @@ const navigationLinks = [ { href: "/Tasks", label: "Tasks", active: false }, ]; + export default function NavBarComponent() { + const { user, logout, isAuthenticated } = useAuth(); + const router = useRouter(); + + const handleLogout = async () => { + await logout(); + router.push('/LogIn'); + }; + + // Navigation links based on authentication status + const navigationLinks = isAuthenticated + ? [ + { href: "/", label: "Home", active: true }, + { href: "/Tasks", label: "Tasks" }, + { href: "/StudySession", label: "Study Session" }, + { href: "#", label: "Features" }, + ] + : [ + { href: "#", label: "Home", active: true }, + { href: "#", label: "Features" }, + { href: "#", label: "Pricing" }, + { href: "#", label: "About" }, + ]; + return (
@@ -104,12 +132,25 @@ export default function NavBarComponent() {
{/* Right side */}
- - + {isAuthenticated ? ( + <> + + Welcome, {user?.username} + + + + ) : ( + <> + + + + )}
diff --git a/components/ProtectedRoute.jsx b/components/ProtectedRoute.jsx new file mode 100644 index 0000000..e69de29 diff --git a/components/ui/popover.jsx b/components/ui/popover.jsx index 6ef38a8..8c74827 100644 --- a/components/ui/popover.jsx +++ b/components/ui/popover.jsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import { Popover as PopoverPrimitive } from "radix-ui" +import * as PopoverPrimitive from "@radix-ui/react-popover" import { cn } from "@/lib/utils" diff --git a/contexts/AuthContext.js b/contexts/AuthContext.js new file mode 100644 index 0000000..0f96208 --- /dev/null +++ b/contexts/AuthContext.js @@ -0,0 +1,55 @@ +"use client"; +import React, { createContext, useContext, useState, useEffect } from "react"; +import api from "@/utils/api"; + +const AuthContext = createContext(); + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +}; + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + checkAuthStatus(); + }, []); + + const checkAuthStatus = async () => { + try { + const response = await api.get("/auth/me"); + if (response.data.user) { + setUser(response.data.user); + } + } catch (error) { + console.log("Not authenticated"); + setUser(null); + } finally { + setLoading(false); + } + }; + + const logout = async () => { + try { + await api.post("/auth/logout"); + setUser(null); + } catch (error) { + console.error("Logout error:", error); + } + }; + + const value = { + user, + loading, + checkAuthStatus, + logout, + isAuthenticated: !!user, + }; + + return {children}; +}; diff --git a/cookies.txt b/cookies.txt new file mode 100644 index 0000000..c31d989 --- /dev/null +++ b/cookies.txt @@ -0,0 +1,4 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + diff --git a/package-lock.json b/package-lock.json index b0756cd..a7b95bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@google-cloud/local-auth": "^3.0.1", "@gsap/react": "^2.1.2", "@icons-pack/react-simple-icons": "^13.7.0", "@mui/material": "^7.3.1", @@ -29,8 +30,11 @@ "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "email-validator": "^2.0.4", - "framer-motion": "^12.23.12", + + "google-auth-library": "^10.2.1", + "gsap": "^3.13.0", "harden-react-markdown": "^1.0.2", "katex": "^0.16.22", @@ -41,6 +45,12 @@ "next-themes": "^0.4.6", "ogl": "^1.0.11", "radix-ui": "^1.4.2", + + "react": "19.1.0", + "react-cookie": "^8.0.1", + "react-day-picker": "^9.9.0", + "react-dom": "19.1.0", + "react": "^19.1.1", "react-day-picker": "^9.8.1", "react-dom": "^19.1.1", @@ -50,6 +60,7 @@ "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "shiki": "^3.9.2", + "tailwind-merge": "^3.3.1", "three": "^0.179.1", "use-stick-to-bottom": "^1.1.1", @@ -221,6 +232,30 @@ "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", "license": "MIT" + + }, + "node_modules/@emnapi/core": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@dimforge/rapier3d-compat": { "version": "0.12.0", @@ -553,6 +588,110 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, + "node_modules/@google-cloud/local-auth": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/local-auth/-/local-auth-3.0.1.tgz", + "integrity": "sha512-YJ3GFbksfHyEarbVHPSCzhKpjbnlAhdzg2SEf79l6ODukrSM1qUOqfopY232Xkw26huKSndyzmJz+A6b2WYn7Q==", + "license": "Apache-2.0", + "dependencies": { + "arrify": "^2.0.1", + "google-auth-library": "^9.0.0", + "open": "^7.0.3", + "server-destroy": "^1.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/@gsap/react": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@gsap/react/-/react-2.1.2.tgz", @@ -3414,6 +3553,18 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + "node_modules/@types/estree-jsx": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", @@ -3430,6 +3581,7 @@ "license": "MIT", "dependencies": { "@types/unist": "*" + } }, "node_modules/@types/json-schema": { @@ -3907,6 +4059,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4170,6 +4331,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -4336,6 +4506,15 @@ "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==", "license": "MIT" }, + + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -4356,6 +4535,7 @@ }, "funding": { "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/brace-expansion": { @@ -4382,10 +4562,23 @@ "node": ">=8" } }, + + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "node_modules/browserslist": { "version": "4.25.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", + "dev": true, "funding": [ { @@ -4770,6 +4963,15 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -4977,6 +5179,15 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5247,6 +5458,16 @@ "node": ">= 0.4" } }, + + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -5260,6 +5481,7 @@ "integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==", "dev": true, "license": "ISC" + }, "node_modules/email-validator": { "version": "2.0.4", @@ -5957,12 +6179,14 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -6030,11 +6254,36 @@ "reusify": "^1.0.4" } }, + + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", @@ -6175,11 +6424,30 @@ "node": ">= 6" } }, + + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, + "license": "MIT", "engines": { "node": "*" @@ -6271,6 +6539,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.1.tgz", + "integrity": "sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", + "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -6438,12 +6734,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + + "node_modules/google-auth-library": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.2.1.tgz", + "integrity": "sha512-HMxFl2NfeHYnaL1HoRIN1XgorKS+6CDaM+z9LSSN+i/nKDDL4KFFEWogMXu7jV4HZQy2MsxpY+wA5XIf3w410A==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^7.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz", + "integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + "node_modules/glsl-noise": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", "license": "MIT" - }, + + "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -6475,6 +6800,19 @@ "integrity": "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==", "license": "Standard 'no charge' license: https://gsap.com/standard-license." }, + + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + "node_modules/harden-react-markdown": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/harden-react-markdown/-/harden-react-markdown-1.0.2.tgz", @@ -6483,6 +6821,7 @@ "peerDependencies": { "react": ">=16.8.0", "react-markdown": ">=9.0.0" + } }, "node_modules/has-bigints": { @@ -6782,6 +7121,21 @@ "react-is": "^16.7.0" } }, + + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -6822,6 +7176,7 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -7092,6 +7447,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + "node_modules/is-decimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", @@ -7100,6 +7470,7 @@ "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/is-extglob": { @@ -7299,6 +7670,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -7396,6 +7779,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -7504,6 +7899,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -7560,6 +7964,27 @@ "node": ">=4.0" } }, + + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + "node_modules/katex": { "version": "0.16.22", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", @@ -7574,6 +7999,7 @@ }, "bin": { "katex": "cli.js" + } }, "node_modules/keyv": { @@ -9173,6 +9599,44 @@ "node": "^10 || ^12 || >=14" } }, + + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -9198,6 +9662,7 @@ "license": "MIT", "engines": { "node": ">=0.10.0" + } }, "node_modules/object-assign": { @@ -9332,6 +9797,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + "node_modules/ogl": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ogl/-/ogl-1.0.11.tgz", @@ -9353,6 +9834,7 @@ "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" + } }, "node_modules/optionator": { @@ -9880,6 +10362,29 @@ "node": ">=0.10.0" } }, + + "node_modules/react-cookie": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-8.0.1.tgz", + "integrity": "sha512-QNdAd0MLuAiDiLcDU/2s/eyKmmfMHtjPUKJ2dZ/5CcQ9QKUium4B3o61/haq6PQl/YWFqC5PO8GvxeHKhy3GFA==", + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.6", + "hoist-non-react-statics": "^3.3.2", + "universal-cookie": "^8.0.0" + }, + "peerDependencies": { + "react": ">= 16.3.0" + } + }, + "node_modules/react-day-picker": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.9.0.tgz", + "integrity": "sha512-NtkJbuX6cl/VaGNb3sVVhmMA6LSMnL5G3xNL+61IyoZj0mUZFWTg4hmj7PHjIQ8MXN9dHWhUHFoJWG6y60DKSg==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "node_modules/react-day-picker": { "version": "9.8.1", "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.8.1.tgz", @@ -9887,6 +10392,7 @@ "license": "MIT", "dependencies": { "@date-fns/tz": "^1.2.0", + "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, @@ -10439,6 +10945,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -10493,6 +11019,12 @@ "node": ">=10" } }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -11502,6 +12034,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -11713,6 +12251,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + + "node_modules/universal-cookie": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-8.0.1.tgz", + "integrity": "sha512-B6ks9FLLnP1UbPPcveOidfvB9pHjP+wekP2uRYB9YDfKVpvcjKgy1W5Zj+cEXJ9KTPnqOKGfVDQBmn8/YCQfRg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2" + "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -11826,6 +12373,7 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" + } }, "node_modules/unrs-resolver": { @@ -11965,6 +12513,46 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12066,6 +12654,7 @@ "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", "license": "MIT" }, + "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index c7ce540..42f1850 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@google-cloud/local-auth": "^3.0.1", "@gsap/react": "^2.1.2", "@icons-pack/react-simple-icons": "^13.7.0", "@mui/material": "^7.3.1", @@ -31,8 +32,13 @@ "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "email-validator": "^2.0.4", + + "google-auth-library": "^10.2.1", + "framer-motion": "^12.23.12", + "gsap": "^3.13.0", "harden-react-markdown": "^1.0.2", "katex": "^0.16.22", @@ -43,6 +49,12 @@ "next-themes": "^0.4.6", "ogl": "^1.0.11", "radix-ui": "^1.4.2", + + "react": "19.1.0", + "react-cookie": "^8.0.1", + "react-day-picker": "^9.9.0", + "react-dom": "19.1.0", + "react": "^19.1.1", "react-day-picker": "^9.8.1", "react-dom": "^19.1.1", @@ -52,6 +64,7 @@ "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "shiki": "^3.9.2", + "tailwind-merge": "^3.3.1", "three": "^0.179.1", "use-stick-to-bottom": "^1.1.1", diff --git a/utils/api.js b/utils/api.js new file mode 100644 index 0000000..43fefa0 --- /dev/null +++ b/utils/api.js @@ -0,0 +1,51 @@ +import axios from "axios"; + +const api = axios.create({ + baseURL: "http://localhost:8080", + withCredentials: true, // ESSENTIAL: Sends cookies with every request + headers: { + "Content-Type": "application/json", + }, +}); + +// Add request interceptor for debugging +api.interceptors.request.use( + (config) => { + console.log( + `๐Ÿš€ ${config.method?.toUpperCase()} ${config.baseURL}${config.url}` + ); + if (config.data) { + console.log("๐Ÿ“ฆ Request data:", config.data); + } + return config; + }, + (error) => { + console.error("Request Error:", error); + return Promise.reject(error); + } +); + +// Optional: Add request/response interceptors for global error handling +api.interceptors.response.use( + (response) => { + console.log(`โœ… ${response.status} Response:`, response.data); + return response; + }, + (error) => { + console.error( + `โŒ ${error.response?.status || "Network"} Error:`, + error.response?.data || error.message + ); + + if (error.response?.status === 401 || error.response?.status === 403) { + // Token expired or invalid - redirect to login + console.log("๐Ÿ”’ Token expired or invalid, redirecting to login..."); + if (typeof window !== "undefined") { + window.location.href = "/LogIn"; + } + } + return Promise.reject(error); + } +); + +export default api; diff --git a/utils/calendarService.js b/utils/calendarService.js new file mode 100644 index 0000000..12af653 --- /dev/null +++ b/utils/calendarService.js @@ -0,0 +1,260 @@ +// Google Calendar Integration Service - PRODUCTION MODE +import api from "./api"; + +class CalendarService { + // Helper function to validate if a date is valid + static isValidDate(dateString) { + if (!dateString) return false; + + const date = new Date(dateString); + return date instanceof Date && !isNaN(date.getTime()); + } + + // Helper function to format date properly for calendar events + static formatDateForCalendar(deadline, timeString) { + try { + if (!deadline) { + throw new Error("No deadline provided"); + } + + // Get the date part (YYYY-MM-DD format) + let dateOnly; + if (deadline instanceof Date) { + dateOnly = deadline.toISOString().split("T")[0]; + } else if (typeof deadline === "string") { + dateOnly = deadline.split("T")[0]; // Get just the date part + } else { + dateOnly = new Date(deadline).toISOString().split("T")[0]; + } + + // Parse the time (format: "HH:MM:SS" or "HH:MM") + const timeParts = timeString.split(":"); + const hours = parseInt(timeParts[0], 10); + const minutes = parseInt(timeParts[1], 10); + const seconds = timeParts[2] ? parseInt(timeParts[2], 10) : 0; + + // Create a proper Date object in local time + const [year, month, day] = dateOnly + .split("-") + .map((num) => parseInt(num, 10)); + const date = new Date(year, month - 1, day, hours, minutes, seconds); // month is 0-indexed + + // Validate the date + if (isNaN(date.getTime())) { + throw new Error(`Invalid date: ${deadline} with time ${timeString}`); + } + + return date.toISOString(); + } catch (error) { + console.error("Error formatting date for calendar:", error); + // Return a default date (tomorrow at the specified time) as fallback + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const timeParts = timeString.split(":"); + const hours = parseInt(timeParts[0], 10) || 9; + const minutes = parseInt(timeParts[1], 10) || 0; + tomorrow.setHours(hours, minutes, 0, 0); + return tomorrow.toISOString(); + } + } + + // Create a calendar event for a task deadline + static async createTaskReminder(task) { + try { + // Validate that task has required fields + if (!task) { + throw new Error("Task object is required"); + } + + if (!task.deadline) { + throw new Error("Task deadline is required for calendar reminder"); + } + + if (!this.isValidDate(task.deadline)) { + throw new Error(`Invalid deadline format: ${task.deadline}`); + } + + if (!task.assignment) { + throw new Error("Task assignment is required for calendar reminder"); + } + + console.log("Creating calendar reminder for task:", task); + console.log("Task deadline input:", task.deadline); + console.log("Making API call to:", `/api/calendar/sync-task/${task.id}`); + + // Prepare calendar event data for the backend + const startTime = this.formatDateForCalendar(task.deadline, "09:00:00"); + const endTime = this.formatDateForCalendar(task.deadline, "10:00:00"); + + console.log("Formatted startTime:", startTime); + console.log("Formatted endTime:", endTime); + + const calendarEventData = { + summary: `๐Ÿ“ Task Due: ${task.assignment}`, + description: `Task: ${task.assignment}\nDue: ${ + task.deadline + }\nPriority: ${ + task.priority || "medium" + }\n\nCreated via LockIn Task Manager`, + startTime: startTime, + endTime: endTime, + colorId: this.getPriorityColor(task.priority), + // Google Calendar notification settings + reminders: { + useDefault: false, + overrides: [ + { method: "email", minutes: 1440 }, // 1 day before + { method: "email", minutes: 10 }, // 10 minutes before + { method: "popup", minutes: 60 }, // 1 hour before + ], + }, + }; + + console.log("Sending calendar event data:", calendarEventData); + + // Use the backend's sync-task endpoint with proper request body + const response = await api.post( + `/api/calendar/sync-task/${task.id}`, + calendarEventData + ); + + console.log("Calendar sync successful:", response.data); + return response.data; + } catch (error) { + console.error("Full calendar error details:", { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + message: error.message, + url: error.config?.url, + }); + + // Check if this is a 404 or 400 error (backend endpoint not implemented) + if (error.response?.status === 404 || error.response?.status === 400) { + console.log( + "๐Ÿ“… Calendar API endpoints not implemented yet - skipping calendar reminder" + ); + throw new Error("Calendar API not available"); + } + + // Handle 500 errors (backend internal error) + if (error.response?.status === 500) { + console.error( + "๐Ÿ“… Backend calendar service error - calendar reminder skipped" + ); + throw new Error("Calendar service temporarily unavailable"); + } + + console.error("Error creating calendar reminder:", error); + throw error; + } + } + + // Update calendar event when task is modified + static async updateTaskReminder(task, calendarEventId) { + try { + // Prepare updated calendar event data + const startTime = this.formatDateForCalendar(task.deadline, "09:00:00"); + const endTime = this.formatDateForCalendar(task.deadline, "10:00:00"); + + const calendarEventData = { + summary: `๐Ÿ“ Task Due: ${task.assignment}`, + description: `Task: ${task.assignment}\nDue: ${ + task.deadline + }\nPriority: ${ + task.priority || "medium" + }\n\nUpdated via LockIn Task Manager`, + startTime: startTime, + endTime: endTime, + colorId: this.getPriorityColor(task.priority), + // Google Calendar notification settings + reminders: { + useDefault: false, + overrides: [ + { method: "email", minutes: 1440 }, // 1 day before + { method: "email", minutes: 10 }, // 10 minutes before + { method: "popup", minutes: 60 }, // 1 hour before + ], + }, + }; + + console.log("Updating calendar event with data:", calendarEventData); + + // For updates, we use the same sync endpoint with updated data + const response = await api.post( + `/api/calendar/sync-task/${task.id}`, + calendarEventData + ); + + return response.data; + } catch (error) { + // Check if this is a 404 or 400 error (backend endpoint not implemented) + if (error.response?.status === 404 || error.response?.status === 400) { + console.log( + "๐Ÿ“… Calendar API endpoints not implemented yet - skipping calendar update" + ); + throw new Error("Calendar API not available"); + } + + console.error("Error updating calendar reminder:", error); + throw error; + } + } + + // Delete calendar event when task is deleted + static async deleteTaskReminder(task) { + try { + // Use the task ID for the delete endpoint + await api.delete(`/api/calendar/sync-task/${task.id}`); + } catch (error) { + // Check if this is a 404 or 400 error (backend endpoint not implemented) + if (error.response?.status === 404 || error.response?.status === 400) { + console.log( + "๐Ÿ“… Calendar API endpoints not implemented yet - skipping calendar deletion" + ); + return; // Don't throw error for delete operations when API not available + } + + console.error("Error deleting calendar reminder:", error); + throw error; + } + } + + // Get color based on task priority + static getPriorityColor(priority) { + const colorMap = { + high: "11", // Red + medium: "5", // Yellow + low: "10", // Green + }; + return colorMap[priority] || "1"; // Default blue + } + + // Check if user has granted calendar permissions + static async checkCalendarPermissions() { + try { + const response = await api.get("/api/calendar/permissions"); + return response.data.hasPermissions; + } catch (error) { + // Check if this is a 404 or 400 error (backend endpoint not implemented) + if (error.response?.status === 404 || error.response?.status === 400) { + console.log( + "๐Ÿ“… Calendar API endpoints not implemented yet - calendar features disabled" + ); + return false; + } + + console.error("Error checking calendar permissions:", error); + return false; + } + } + + // Request additional calendar permissions + static requestCalendarPermissions() { + // Redirect to backend calendar OAuth with a calendar-specific parameter + window.location.href = + "http://localhost:8080/auth/google/calendar?redirect=tasks"; + } +} + +export default CalendarService; diff --git a/utils/calendarService_MOCK.js b/utils/calendarService_MOCK.js new file mode 100644 index 0000000..579f34c --- /dev/null +++ b/utils/calendarService_MOCK.js @@ -0,0 +1,162 @@ +// Google Calendar Integration Service - WITH MOCK TESTING +import api from "./api"; + +class CalendarService { + // Create a calendar event for a task deadline + static async createTaskReminder(task) { + try { + // ๐Ÿงช TEMPORARY: Mock for testing without backend + if (process.env.NODE_ENV === "development") { + console.log("๐Ÿงช MOCK: Creating calendar reminder for task:", task); + await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API delay + return { + id: "mock_event_" + Date.now(), + htmlLink: "https://calendar.google.com/calendar/u/0/r/event?eid=mock", + }; + } + + const eventData = { + summary: `Assignment Due: ${task.assignment}`, + description: `Class: ${task.className}\nAssignment: ${task.assignment}\nDescription: ${task.description}\nPriority: ${task.priority}`, + start: { + dateTime: new Date(task.deadline + "T09:00:00").toISOString(), + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }, + end: { + dateTime: new Date(task.deadline + "T10:00:00").toISOString(), + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }, + reminders: { + useDefault: false, + overrides: [ + { method: "email", minutes: 24 * 60 }, // 1 day before + { method: "popup", minutes: 60 }, // 1 hour before + { method: "email", minutes: 10 }, // 10 minutes before + ], + }, + colorId: this.getPriorityColor(task.priority), + }; + + const response = await api.post("/api/calendar/events", { + taskId: task.id, + eventData, + }); + + return response.data; + } catch (error) { + console.error("Error creating calendar reminder:", error); + throw error; + } + } + + // Update calendar event when task is modified + static async updateTaskReminder(task, calendarEventId) { + try { + // ๐Ÿงช TEMPORARY: Mock for testing without backend + if (process.env.NODE_ENV === "development") { + console.log("๐Ÿงช MOCK: Updating calendar reminder for task:", task); + await new Promise((resolve) => setTimeout(resolve, 500)); + return { + id: calendarEventId, + htmlLink: "https://calendar.google.com/calendar/u/0/r/event?eid=mock", + }; + } + + const eventData = { + summary: `Assignment Due: ${task.assignment}`, + description: `Class: ${task.className}\nAssignment: ${task.assignment}\nDescription: ${task.description}\nPriority: ${task.priority}`, + start: { + dateTime: new Date(task.deadline + "T09:00:00").toISOString(), + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }, + end: { + dateTime: new Date(task.deadline + "T10:00:00").toISOString(), + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }, + colorId: this.getPriorityColor(task.priority), + }; + + const response = await api.put( + `/api/calendar/events/${calendarEventId}`, + { + taskId: task.id, + eventData, + } + ); + + return response.data; + } catch (error) { + console.error("Error updating calendar reminder:", error); + throw error; + } + } + + // Delete calendar event when task is deleted + static async deleteTaskReminder(calendarEventId) { + try { + // ๐Ÿงช TEMPORARY: Mock for testing without backend + if (process.env.NODE_ENV === "development") { + console.log("๐Ÿงช MOCK: Deleting calendar reminder:", calendarEventId); + await new Promise((resolve) => setTimeout(resolve, 500)); + return; + } + + await api.delete(`/api/calendar/events/${calendarEventId}`); + } catch (error) { + console.error("Error deleting calendar reminder:", error); + throw error; + } + } + + // Get color based on task priority + static getPriorityColor(priority) { + const colorMap = { + high: "11", // Red + medium: "5", // Yellow + low: "10", // Green + }; + return colorMap[priority] || "1"; // Default blue + } + + // Check if user has granted calendar permissions + static async checkCalendarPermissions() { + try { + // ๐Ÿงช TEMPORARY: Mock for testing without backend + if (process.env.NODE_ENV === "development") { + console.log( + "๐Ÿงช MOCK: Checking calendar permissions (backend not implemented)" + ); + await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate API delay + return false; // Change to true to test "connected" state + } + + const response = await api.get("/api/calendar/permissions"); + return response.data.hasPermissions; + } catch (error) { + console.error("Error checking calendar permissions:", error); + return false; + } + } + + // Request additional calendar permissions + static requestCalendarPermissions() { + // ๐Ÿงช TEMPORARY: Mock for testing without backend + if (process.env.NODE_ENV === "development") { + console.log("๐Ÿงช MOCK: Would request calendar permissions here"); + const proceed = confirm( + "๐Ÿงช MOCK MODE: This would normally redirect to Google OAuth.\n\n" + + "Backend route /auth/google/calendar not implemented yet.\n\n" + + "Click OK to simulate successful connection (page will reload)." + ); + if (proceed) { + // Simulate successful connection by reloading + window.location.reload(); + } + return; + } + + window.location.href = "http://localhost:8080/auth/google/calendar"; + } +} + +export default CalendarService;