From c5368cf48858a39f58662e5df85d6717cb780945 Mon Sep 17 00:00:00 2001 From: Benjamin Ayala Date: Thu, 14 Aug 2025 11:10:21 -0400 Subject: [PATCH 1/4] Fixed the return for status --- app/Tasks/[userId]/page.jsx | 22 ++++++++++-------- package-lock.json | 46 ++++++++++++++++++++++++++++++++++++- package.json | 2 ++ 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/app/Tasks/[userId]/page.jsx b/app/Tasks/[userId]/page.jsx index 2505abd..ae19616 100644 --- a/app/Tasks/[userId]/page.jsx +++ b/app/Tasks/[userId]/page.jsx @@ -23,7 +23,7 @@ export default function TasksPage({ params }) { className: "", assignment: "", description: "", - status: "pending", + status: "Pending", deadline: "", priority: "medium", }); @@ -91,14 +91,6 @@ const handleEditChange = (e) => { } }; -// 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 => @@ -109,7 +101,17 @@ const handleEditChange = (e) => { return (
-

Your Tasks (User {userId})

+

+ Assignment Tracker +

{/* Filter UI */}
=6.9.0" } }, + "node_modules/@date-fns/tz": { + "version": "1.4.1", + "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", @@ -4898,6 +4906,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -4958,7 +4982,6 @@ "url": "https://github.com/sponsors/ljharb" } }, -======= "node_modules/delaunator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", @@ -7809,6 +7832,27 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "9.8.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.8.1.tgz", + "integrity": "sha512-kMcLrp3PfN/asVJayVv82IjF3iLOOxuH5TNFWezX6lS/T8iVRFPTETpHl3TUSTH99IDMZLubdNPJr++rQctkEw==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.2.0", + "date-fns": "^4.1.0", + "date-fns-jalali": "^4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", diff --git a/package.json b/package.json index 22a128d..6212003 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "email-validator": "^2.0.4", "gsap": "^3.13.0", "lucide-react": "^0.534.0", @@ -27,6 +28,7 @@ "next-themes": "^0.4.6", "radix-ui": "^1.4.2", "react": "19.1.0", + "react-day-picker": "^9.8.1", "react-dom": "19.1.0", "tailwind-merge": "^3.3.1", "zxcvbn": "^4.4.2" From 197b4cedfe915058f42ff8b609a93c19ea343761 Mon Sep 17 00:00:00 2001 From: Benjamin Ayala Date: Thu, 14 Aug 2025 11:17:16 -0400 Subject: [PATCH 2/4] merged --- app/StudySession/page.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/StudySession/page.js b/app/StudySession/page.js index 735d406..9aaba29 100644 --- a/app/StudySession/page.js +++ b/app/StudySession/page.js @@ -134,7 +134,7 @@ function StudyTimer() {
Date: Tue, 19 Aug 2025 01:07:33 -0400 Subject: [PATCH 3/4] Implemented user authorization --- app/LogIn/page.js | 35 ++- app/SignUp/page.js | 78 ++++++- app/Tasks/[userId]/page.jsx | 351 ---------------------------- app/Tasks/page.jsx | 428 ++++++++++++++++++++++++++++++++++ app/layout.js | 19 +- components/NavBar.jsx | 59 +++-- components/ProtectedRoute.jsx | 0 components/ui/popover.jsx | 2 +- contexts/AuthContext.js | 55 +++++ cookies.txt | 4 + package-lock.json | 55 ++++- package.json | 3 +- utils/api.js | 51 ++++ 13 files changed, 745 insertions(+), 395 deletions(-) delete mode 100644 app/Tasks/[userId]/page.jsx create mode 100644 app/Tasks/page.jsx create mode 100644 components/ProtectedRoute.jsx create mode 100644 contexts/AuthContext.js create mode 100644 cookies.txt create mode 100644 utils/api.js diff --git a/app/LogIn/page.js b/app/LogIn/page.js index 7382252..f875118 100644 --- a/app/LogIn/page.js +++ b/app/LogIn/page.js @@ -10,13 +10,21 @@ 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"; function LogIn() { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const router = useRouter(); + const { checkAuthStatus } = useAuth(); const handleUsernameChange = (event) => { setUsername(event.target.value); @@ -28,13 +36,29 @@ function LogIn() { const handleLogin = async (event) => { event.preventDefault(); + setLoading(true); + setError(""); + try { - const response = await axios.post(`http://localhost:8080/auth/login`, { + const response = await api.post("/auth/login", { username, password, }); + + console.log("Login successful:", response.data); + + // Update auth context + await checkAuthStatus(); + + // Redirect to dashboard + router.push("/"); } catch (error) { - console.log("error", error.response); + console.error("Login error:", error); + setError( + error.response?.data?.error || "Login failed. Please try again." + ); + } finally { + setLoading(false); } }; @@ -85,9 +109,10 @@ function LogIn() { onChange={handlePasswordChange} />
- + {error && {error}} diff --git a/app/SignUp/page.js b/app/SignUp/page.js index 55b51a7..ff507e2 100644 --- a/app/SignUp/page.js +++ b/app/SignUp/page.js @@ -12,10 +12,12 @@ 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 } from "@mui/material"; -import axios from "axios"; +import api from "@/utils/api"; +import { useAuth } from "@/contexts/AuthContext"; import Link from "next/link"; export default function Signup() { @@ -25,9 +27,14 @@ 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 router = useRouter(); + const { checkAuthStatus } = useAuth(); + const handleFirstNameChange = (event) => { setFirstName(event.target.value); }; @@ -79,20 +86,66 @@ export default function Signup() { const handleSignUp = async (event) => { event.preventDefault(); - if (validate(email)) { - setValidation(true); + setLoading(true); + setError(""); + setValidation(false); + + // Validate email + if (!validate(email)) { + setError("Please enter a valid email address"); + setLoading(false); + return; } + // Validate password strength + if (passwordErrors.length > 0 || passwordStrength?.score < 2) { + setError("Please choose a stronger password"); + setLoading(false); + return; + } + + console.log("๐Ÿ“ Starting signup process..."); + console.log("๐Ÿ” Signup data:", { + firstName, + lastName, + username, + email, + password: "***", + }); + try { - const res = await axios.post(`http://localhost:8080/auth/signup`, { + 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:", error); + 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); } }; @@ -194,16 +247,21 @@ export default function Signup() { ) : null} - {validation ? ( - Signed up successfully - ) : null} + {error && {error}} + {validation && ( + + Signed up successfully! Redirecting... + + )} {/* */} diff --git a/app/Tasks/[userId]/page.jsx b/app/Tasks/[userId]/page.jsx deleted file mode 100644 index ae19616..0000000 --- a/app/Tasks/[userId]/page.jsx +++ /dev/null @@ -1,351 +0,0 @@ -"use client"; -import { useEffect, useState } from "react"; -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 axios from "axios"; -import "../tasks.css"; - -export default function TasksPage({ params }) { - const userId = params.userId; - const [tasks, setTasks] = useState([]); - const [showNewRow, setShowNewRow] = useState(false); - const [editTask, setEditTask] = useState(null); - const [newTask, setNewTask] = useState({ - className: "", - assignment: "", - description: "", - status: "Pending", - deadline: "", - priority: "medium", - }); - // Filter states - const [filterClassName, setFilterClassName] = useState(""); - const [filterStatus, setFilterStatus] = useState(""); - const [filterPriority, setFilterPriority] = useState(""); - - - const [calendarOpen, setCalendarOpen] = useState(false); - const [editCalendarOpen, setEditCalendarOpen] = useState(false); - - const fetchTasks = async () => { - 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); - } - }; - - - // 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 -

- {/* 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..5b14f3a --- /dev/null +++ b/app/Tasks/page.jsx @@ -0,0 +1,428 @@ +"use client"; +import { useEffect, useState } from "react"; +import { useAuth } from "@/contexts/AuthContext"; +import { useRouter } 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 "./tasks.css"; + +export default function TasksPage() { + const { user, loading } = useAuth(); + const router = useRouter(); + const [tasks, setTasks] = useState([]); + const [tasksLoading, setTasksLoading] = useState(true); + const [error, setError] = useState(""); + const [showNewRow, setShowNewRow] = useState(false); + const [editTask, setEditTask] = useState(null); + const [newTask, setNewTask] = useState({ + className: "", + assignment: "", + description: "", + status: "pending", // Match ENUM exactly + deadline: "", + priority: "medium", + }); + // 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(); + } + }, [user]); + + const fetchTasks = async () => { + try { + setTasksLoading(true); + const response = await api.get('/api/tasks'); + setTasks(response.data); + } catch (error) { + console.error("Error fetching tasks:", error); + console.error("Error details:", error.response?.data); + setError('Failed to load tasks'); + } finally { + setTasksLoading(false); + } + }; + + const handleDelete = async (taskId) => { + try { + 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); + setTasks([...tasks, response.data]); + setShowNewRow(false); + setNewTask({ + className: "", + assignment: "", + description: "", + status: "pending", + deadline: "", + priority: "medium", + }); + } 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); + setTasks(tasks.map(task => task.id === editTask.id ? response.data : 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}!

+
+ + {error && ( +
+ {error} + +
+ )} + + {/* 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 + + + + + + + + + { + 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() : "No deadline"} + + {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/layout.js b/app/layout.js index 8927f37..2c17a64 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"; const poppins = Poppins({ @@ -31,14 +32,16 @@ export default function RootLayout({ children }) { - - {children} - + + + {children} + + diff --git a/components/NavBar.jsx b/components/NavBar.jsx index e6bd982..68baf97 100644 --- a/components/NavBar.jsx +++ b/components/NavBar.jsx @@ -1,3 +1,4 @@ +"use client"; import { Button } from "@/components/ui/button"; import { NavigationMenu, @@ -10,17 +11,34 @@ 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 }, - { href: "#", label: "Features" }, - { href: "#", label: "Pricing" }, - { href: "#", label: "About" }, -]; - 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 +122,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 326cd86..a90e736 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,8 @@ "next-themes": "^0.4.6", "radix-ui": "^1.4.2", "react": "19.1.0", - "react-day-picker": "^9.8.1", + "react-cookie": "^8.0.1", + "react-day-picker": "^9.9.0", "react-dom": "19.1.0", "tailwind-merge": "^3.3.1", "zxcvbn": "^4.4.2" @@ -3513,6 +3514,18 @@ "dev": true, "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/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4684,6 +4697,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", @@ -7832,13 +7854,27 @@ "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.8.1", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.8.1.tgz", - "integrity": "sha512-kMcLrp3PfN/asVJayVv82IjF3iLOOxuH5TNFWezX6lS/T8iVRFPTETpHl3TUSTH99IDMZLubdNPJr++rQctkEw==", + "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.2.0", + "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, @@ -8860,6 +8896,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/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", diff --git a/package.json b/package.json index 6212003..efb6c6c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "next-themes": "^0.4.6", "radix-ui": "^1.4.2", "react": "19.1.0", - "react-day-picker": "^9.8.1", + "react-cookie": "^8.0.1", + "react-day-picker": "^9.9.0", "react-dom": "19.1.0", "tailwind-merge": "^3.3.1", "zxcvbn": "^4.4.2" 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; From 57c553d18d4ba50a5b7f42ad7289c81f7c99ad6e Mon Sep 17 00:00:00 2001 From: Benjamin Ayala Date: Tue, 19 Aug 2025 15:27:17 -0400 Subject: [PATCH 4/4] Implemented google calender api, when the user chooses a deadline it creates an event on google calender --- app/LogIn/page.js | 12 +- app/SignUp/page.js | 11 +- app/Tasks/page.jsx | 182 +++++++++- app/auth/callback/page.js | 45 +++ app/dashboard/page.js | 0 components/CalendarNotification.jsx | 47 +++ package-lock.json | 492 ++++++++++++++++++++++++++++ package.json | 2 + utils/calendarService.js | 260 +++++++++++++++ utils/calendarService_MOCK.js | 162 +++++++++ 10 files changed, 1201 insertions(+), 12 deletions(-) create mode 100644 app/auth/callback/page.js create mode 100644 app/dashboard/page.js create mode 100644 components/CalendarNotification.jsx create mode 100644 utils/calendarService.js create mode 100644 utils/calendarService_MOCK.js diff --git a/app/LogIn/page.js b/app/LogIn/page.js index f875118..ebf9509 100644 --- a/app/LogIn/page.js +++ b/app/LogIn/page.js @@ -62,6 +62,11 @@ function LogIn() { } }; + const handleGoogleLogin = () => { + // Redirect to backend Google OAuth + window.location.href = `http://localhost:8080/auth/google`; + }; + return (
@@ -113,7 +118,12 @@ function LogIn() { {loading ? "Logging in..." : "Login"} {error && {error}} -
diff --git a/app/SignUp/page.js b/app/SignUp/page.js index ff507e2..ec51a33 100644 --- a/app/SignUp/page.js +++ b/app/SignUp/page.js @@ -262,9 +262,16 @@ export default function Signup() { Signed up successfully! Redirecting... )} - {/* */} +
diff --git a/app/Tasks/page.jsx b/app/Tasks/page.jsx index 5b14f3a..431a73b 100644 --- a/app/Tasks/page.jsx +++ b/app/Tasks/page.jsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState } from "react"; import { useAuth } from "@/contexts/AuthContext"; -import { useRouter } from "next/navigation"; +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"; @@ -14,16 +14,24 @@ import { 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: "", @@ -31,6 +39,8 @@ export default function TasksPage() { status: "pending", // Match ENUM exactly deadline: "", priority: "medium", + // Add calendar reminder option + createReminder: true, }); // Filter states const [filterClassName, setFilterClassName] = useState(""); @@ -51,9 +61,39 @@ export default function TasksPage() { 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); @@ -61,7 +101,6 @@ export default function TasksPage() { setTasks(response.data); } catch (error) { console.error("Error fetching tasks:", error); - console.error("Error details:", error.response?.data); setError('Failed to load tasks'); } finally { setTasksLoading(false); @@ -70,6 +109,18 @@ export default function TasksPage() { 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) { @@ -81,7 +132,43 @@ export default function TasksPage() { const handleAddTask = async () => { try { const response = await api.post('/api/tasks', newTask); - setTasks([...tasks, response.data]); + 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: "", @@ -90,6 +177,7 @@ export default function TasksPage() { status: "pending", deadline: "", priority: "medium", + createReminder: true, }); } catch (error) { console.error("Error adding task:", error); @@ -105,7 +193,19 @@ export default function TasksPage() { const handleEditTask = async () => { try { const response = await api.put(`/api/tasks/${editTask.id}`, editTask); - setTasks(tasks.map(task => task.id === editTask.id ? response.data : task)); + 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); @@ -153,6 +253,35 @@ export default function TasksPage() {

Welcome back, {user.username}!

+ + {/* Calendar Integration Status */} +
+ {calendarPermissions ? ( +
+ + ๐Ÿ“… Google Calendar Connected + + +
+ ) : ( + + )} +
{error && ( @@ -167,6 +296,11 @@ export default function TasksPage() { )} + {/* Calendar Integration Notification */} + {showCalendarNotification && !calendarPermissions && ( + setShowCalendarNotification(false)} /> + )} + {/* Filter UI */}
{ - setEditTask({ ...editTask, deadline: date ? date.toISOString().split("T")[0] : "" }); + // 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); }} /> @@ -309,7 +447,14 @@ export default function TasksPage() { {task.status} - {task.deadline ? new Date(task.deadline).toLocaleDateString() : "No deadline"} + +
+ {task.deadline ? new Date(task.deadline).toLocaleDateString() : "No deadline"} + {task.calendarEventId && ( + ๐Ÿ“… + )} +
+ {task.priority} @@ -383,7 +528,11 @@ export default function TasksPage() { selected={newTask.deadline ? new Date(newTask.deadline) : undefined} captionLayout="dropdown" onSelect={(date) => { - setNewTask({ ...newTask, deadline: date ? date.toISOString().split("T")[0] : "" }); + // 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); }} /> @@ -406,8 +555,23 @@ export default function TasksPage() { - - +
+ {calendarEnabled && newTask.deadline && ( + + )} +
+ + +
+
)} 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/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/package-lock.json b/package-lock.json index a90e736..19a4109 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", "@mui/material": "^7.3.1", "@mui/x-charts": "^8.9.2", @@ -21,6 +22,7 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "email-validator": "^2.0.4", + "google-auth-library": "^10.2.1", "gsap": "^3.13.0", "lucide-react": "^0.534.0", "next": "15.4.5", @@ -572,6 +574,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", @@ -4151,6 +4257,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", @@ -4373,6 +4488,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", @@ -4465,12 +4589,41 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/bezier-easing": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", "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/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4495,6 +4648,12 @@ "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", @@ -4874,6 +5033,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", @@ -5075,6 +5243,15 @@ "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/email-validator": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/email-validator/-/email-validator-2.0.4.tgz", @@ -5730,6 +5907,12 @@ "node": ">=0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5791,6 +5974,29 @@ "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/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5913,6 +6119,18 @@ "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", @@ -5953,6 +6171,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", @@ -6073,6 +6319,33 @@ "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/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -6105,6 +6378,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/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -6205,6 +6491,19 @@ "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/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6416,6 +6715,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-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -6575,6 +6889,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", @@ -6672,6 +6998,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", @@ -6745,6 +7083,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", @@ -6801,6 +7148,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/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7397,6 +7765,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/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7519,6 +7925,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/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -8142,6 +8564,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", @@ -8196,6 +8638,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", @@ -8729,6 +9177,12 @@ "node": ">=8.0" } }, + "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", @@ -9002,6 +9456,44 @@ "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/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 efb6c6c..b23a6e4 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@google-cloud/local-auth": "^3.0.1", "@gsap/react": "^2.1.2", "@mui/material": "^7.3.1", "@mui/x-charts": "^8.9.2", @@ -22,6 +23,7 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "email-validator": "^2.0.4", + "google-auth-library": "^10.2.1", "gsap": "^3.13.0", "lucide-react": "^0.534.0", "next": "15.4.5", 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;