diff --git a/package-lock.json b/package-lock.json index ee6bc96..acb5040 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "autoprefixer": "10.4.15", "cookie-cutter": "^0.2.0", "cytoscape": "^3.26.0", + "cytoscape-cxtmenu": "^3.5.0", + "cytoscape-dagre": "^2.5.0", "dotenv": "^16.3.1", "eslint-config-next": "13.4.19", "mongodb": "^6.2.0", @@ -1803,6 +1805,34 @@ "node": ">=0.10" } }, + "node_modules/cytoscape-cxtmenu": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/cytoscape-cxtmenu/-/cytoscape-cxtmenu-3.5.0.tgz", + "integrity": "sha512-CoqgKAxvQhmHO5fEgJdBqqR2VjwK1dNkxehc2i0MUMqY0araA13z3oP/9KkprHp9Td++KlVBz6JnncNAD76T0Q==", + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-dagre": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cytoscape-dagre/-/cytoscape-dagre-2.5.0.tgz", + "integrity": "sha512-VG2Knemmshop4kh5fpLO27rYcyUaaDkRw+6PiX4bstpB+QFt0p2oauMrsjVbUamGWQ6YNavh7x2em2uZlzV44g==", + "dependencies": { + "dagre": "^0.8.5" + }, + "peerDependencies": { + "cytoscape": "^3.2.22" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -2979,6 +3009,14 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -4605,9 +4643,9 @@ } }, "node_modules/next-auth": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.3.tgz", - "integrity": "sha512-n1EvmY7MwQMSOkCh6jhI6uBneB6VVtkYELVMEwVaCLD1mBD3IAAucwk+90kgxramW09nSp5drvynwfNCi1JjaQ==", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.5.tgz", + "integrity": "sha512-3RafV3XbfIKk6rF6GlLE4/KxjTcuMCifqrmD+98ejFq73SRoj2rmzoca8u764977lH/Q7jo6Xu6yM+Re1Mz/Og==", "dependencies": { "@babel/runtime": "^7.20.13", "@panva/hkdf": "^1.0.2", @@ -4620,7 +4658,7 @@ "uuid": "^8.3.2" }, "peerDependencies": { - "next": "^12.2.5 || ^13", + "next": "^12.2.5 || ^13 || ^14", "nodemailer": "^6.6.5", "react": "^17.0.2 || ^18", "react-dom": "^17.0.2 || ^18" diff --git a/package.json b/package.json index 712d985..b7d5ab4 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "autoprefixer": "10.4.15", "cookie-cutter": "^0.2.0", "cytoscape": "^3.26.0", + "cytoscape-cxtmenu": "^3.5.0", + "cytoscape-dagre": "^2.5.0", "dotenv": "^16.3.1", "eslint-config-next": "13.4.19", "mongodb": "^6.2.0", diff --git a/src/app/api/auth/[...nextauth]/options.js b/src/app/api/auth/[...nextauth]/options.js index f9cc617..8763af5 100644 --- a/src/app/api/auth/[...nextauth]/options.js +++ b/src/app/api/auth/[...nextauth]/options.js @@ -21,7 +21,7 @@ export const options = { async signIn ({ user, account, profile }) { const existingUser = await getUsers(null, user.email) - if (existingUser) { + if (existingUser && existingUser.length > 0) { return true } else { // Get first and last name @@ -30,17 +30,11 @@ export const options = { const lastName = nameParts.length > 1 ? nameParts.slice(1).join(' ') : '' // Create User - const newUser = await createUser(user.email, firstName, lastName) - return !!newUser // Return true if creation is successful + const createdUser = await createUser(user.email, firstName, lastName) + return !!createdUser // Return true if creation is successful } }, - async session ({ session, user, token }) { - // Assign the user ID to the session to make it available on the client side - session.userId = token.sub // 'sub' is typically the field where the user ID from the provider is stored - return session - }, - async jwt ({ token, user, account, profile, isNewUser }) { // This callback is called whenever a JWT is created. So session.userId is the mongo User _id if (user) { @@ -48,6 +42,12 @@ export const options = { token.sub = mongoUsers[0]._id } return token + }, + + async session ({ session, user, token }) { + // Assign the user ID to the session to make it available on the client side + session.userId = token.sub // 'sub' is typically the field where the user ID from the provider is stored + return session } } diff --git a/src/app/api/mongoDB/createUser/createUser.js b/src/app/api/mongoDB/createUser/createUser.js index 963cf82..7f58293 100644 --- a/src/app/api/mongoDB/createUser/createUser.js +++ b/src/app/api/mongoDB/createUser/createUser.js @@ -22,7 +22,6 @@ import { URI } from '../mongoData.js' export async function createUser (email, firstName = null, lastName = null, username = null, teamIDs = []) { if (mongoose.connection.readyState !== 1) await mongoose.connect(URI) - const user = await User.create({ email, firstName, @@ -30,6 +29,5 @@ export async function createUser (email, firstName = null, lastName = null, user username, teamIDs }) - return user } diff --git a/src/app/api/mongoDB/createUser/route.js b/src/app/api/mongoDB/createUser/route.js index a1b9629..3c859b5 100644 --- a/src/app/api/mongoDB/createUser/route.js +++ b/src/app/api/mongoDB/createUser/route.js @@ -1,5 +1,3 @@ -import { options } from '../../auth/[...nextauth]/options' -import { getServerSession } from 'next-auth/next' import { NextResponse } from 'next/server' import { createUser } from './createUser' @@ -40,11 +38,6 @@ import { createUser } from './createUser' */ export async function POST (request) { try { - const session = await getServerSession(options) - if (!session) { - return NextResponse.json({ success: false, message: 'authentication failed' }, { status: 401 }) - } - const params = await request.json() if (!params.email) { diff --git a/src/app/api/mongoDB/editTask/editTask.js b/src/app/api/mongoDB/editTask/editTask.js new file mode 100644 index 0000000..2716a33 --- /dev/null +++ b/src/app/api/mongoDB/editTask/editTask.js @@ -0,0 +1,34 @@ +import { Task } from '../mongoModels' + +/** + * Creates and stores a new task in the database. This function can be used server side. + * Alternatively the POST request in the route.js should be used on client side (which uses this logic). + * + * @param {string} taskId - The ObjectID of the task. Required. + * @param {string} name - The name of the task. Required. + * @returns {Promise} - A promise that resolves to the newly created task object. + * + * @example + * // Example of using editTask function + * const task = await editTask('65627e3f0deac40cffab844c', 'Develop New Feature'); + * + */ +export async function editTask (taskId, newName) { + try { + // Find the task by its ObjectId and update its name + const updatedTask = await Task.findByIdAndUpdate( + taskId, + { name: newName }, + { new: true } // Return the updated document + ) + + if (!updatedTask) { + throw new Error('Task not found') + } + + return updatedTask + } catch (error) { + console.error('Error updating task:', error) + throw error // Re-throw the error for handling at a higher level + } +} diff --git a/src/app/api/mongoDB/editTask/route.js b/src/app/api/mongoDB/editTask/route.js new file mode 100644 index 0000000..5b7249e --- /dev/null +++ b/src/app/api/mongoDB/editTask/route.js @@ -0,0 +1,69 @@ +import mongoose from 'mongoose' +import { options } from '../../auth/[...nextauth]/options' +import { getServerSession } from 'next-auth/next' +import { NextResponse } from 'next/server' +import { URI } from '../mongoData.js' +import { editTask } from './editTask' + +/** + * Handles a POST request to create and store a new task in the database. This function + * first authenticates the session, then validates the necessary parameters for task creation, + * such as 'name' and 'projectID'. If the parameters are valid and the associated project exists, + * it proceeds to create a new task with the given details. + * + * @param {Object} request - The request object containing the task details. + * @returns {Object} - A response object with a status code,and the task if completed successfully ({message:, task:{}}) + * + * @throws Will throw an error if any of the required fields are missing, if there's an issue connecting + * to the database, or if the session is not authenticated. + * + * @example + * // Example of using this function in a POST request + * fetch(`/api/tasks/createTask`, { + * method: 'POST', + * headers: { 'Content-Type': 'application/json' }, + * body: JSON.stringify({ + * name: 'New Task', + * projectID: 'proj123', + * // ...other task properties + * }), + * }).then(async (response) => { + * const body = await response.json(); + * if (!response.ok) { + * console.error('Task creation failed:', body.message); + * } else { + * console.log('Task created:', body.task); + * } + * }).catch(error => console.error('Error in creating task:', error)); + * + * @property {string} request.body.name - The name of the task. (Required) + */ + +export async function PUT (request) { + try { + const session = await getServerSession(options) + if (!session) { + return NextResponse.json({ success: false, message: 'Authentication failed' }, { status: 401 }) + } + + const params = await request.json() + + if (!params.taskId || !params.newName) { + return NextResponse.json({ message: 'Task ID and new text are required.' }, { status: 400 }) + } + + if (mongoose.connection.readyState !== 1) await mongoose.connect(URI) + + // const projectExists = await Project.exists({ _id: params.projectID }); + // if (!projectExists) { + // return NextResponse.json({ message: `Project ${params.projectID} does not exist.` }, { status: 400 }); + // } + + const updatedTask = await editTask(params.taskId, params.newName) + + return NextResponse.json({ task: updatedTask }, { status: 200 }) + } catch (error) { + console.error('Error updating task:', error) + return NextResponse.json({ message: 'Error updating Task: ' + error }, { status: 500 }) + } +} diff --git a/src/app/page.js b/src/app/page.js index f841903..8682fac 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,9 +1,18 @@ export default function Home () { return ( -
-
- Create or Select a project. +
+ +
+ quayside.app
+ +
+ Ignite Collaborative Productivity +
+
+ Create or select a project to get started... +
+
) } diff --git a/src/components/ContactUsModal.js b/src/components/ContactUsModal.js index 11a4fe7..272d32b 100644 --- a/src/components/ContactUsModal.js +++ b/src/components/ContactUsModal.js @@ -83,7 +83,7 @@ export default function ContactUsModal ({ isOpen, handleClose }) { - @@ -92,9 +92,9 @@ export default function ContactUsModal ({ isOpen, handleClose }) { } return ( -
+
-
+
diff --git a/src/components/Dropdown.js b/src/components/Dropdown.js index 3726366..e63ded3 100644 --- a/src/components/Dropdown.js +++ b/src/components/Dropdown.js @@ -1,5 +1,5 @@ 'use client' -import React from 'react' +import React, { useState } from 'react' import Image from 'next/image' import dropdownIcon from '../../public/svg/dropdown.svg' @@ -21,20 +21,27 @@ import dropdownIcon from '../../public/svg/dropdown.svg' * * @returns {React.Element} The rendered Dropdown button element. */ -const Dropdown = ({ label, clickAction, imagePath }) => { +const Dropdown = ({ label, imagePath, dropdownComponents }) => { + const [dropdownHidden, setDropdown] = useState(false) + return ( +
+ +
+ {dropdownComponents}
- +
) } diff --git a/src/components/Graph.js b/src/components/Graph.js index fba2884..f580a73 100644 --- a/src/components/Graph.js +++ b/src/components/Graph.js @@ -1,6 +1,50 @@ 'use client' import React, { useEffect, useRef, useState } from 'react' import cytoscape from 'cytoscape' +import cxtmenu from 'cytoscape-cxtmenu' +import cydagre from 'cytoscape-dagre' +cytoscape.use(cxtmenu) +cytoscape.use(cydagre) + +const Modal = ({ show, onClose, onSubmit, children }) => { + if (!show) return null + + const modalContentStyle = { + width: '30%', // Adjust the width of the modal as per your requirement + minWidth: '300px', // Minimum width to ensure responsiveness + marginTop: '20px', // Margin from the top to push the modal down a bit + backgroundColor: 'grey', // Background color of the modal + padding: '20px', // Padding inside the modal + borderRadius: '5px', // Rounded corners of the modal + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)', // Box shadow for a subtle depth effect + display: 'flex', // Use flex layout + flexDirection: 'column', // Stack children vertically + alignItems: 'center' // Center-align children horizontally + } + + const modalBackdropStyle = { + position: 'fixed', // Fixes the backdrop in relation to the viewport + top: 0, // Aligns the top edge of the backdrop with the top of the viewport + left: 0, // Aligns the left edge of the backdrop with the left of the viewport + right: 0, // Aligns the right edge of the backdrop with the right of the viewport + bottom: 0, // Aligns the bottom edge of the backdrop with the bottom of the viewport + backgroundColor: 'rgba(0, 0, 0, 0.5)', // Semi-transparent black background + display: 'flex', // Uses flexbox layout + justifyContent: 'center', // Centers children horizontally + alignItems: 'flex-start', // Aligns children to the start of the cross axis, i.e., top + paddingTop: '50px' // Adds padding at the top + } + + return ( +
+
+ {children} + + +
+
+ ) +} /** * A component that fetches task data and renders it as a tree graph using the Cytoscape.js library. @@ -17,9 +61,59 @@ import cytoscape from 'cytoscape' function TreeGraph ({ className, projectID }) { // Fetch Tree data const [tasks, setTasks] = useState(null) - const containerRef = useRef(null) + const [modalOpen, setModalOpen] = useState(false) + const [editLabel, setEditLabel] = useState('') + const [editNode, setEditNode] = useState(null) + + const updateTextInMongoDB = async (taskId, newText) => { + try { + const response = await fetch('/api/mongoDB/editTask', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ taskId, newName: newText }) + }) + + const result = await response.json() + + if (!response.ok) { + console.error('Failed to update:', result.message) + // Handle error feedback to user + } else { + console.log('Text updated successfully in MongoDB.') + // Handle success feedback to user + } + } catch (error) { + console.error('Error updating text in MongoDB:', error) + // Handle error feedback to user + } + } + + // Function to handle the edit command + const handleEdit = (node) => { + setEditNode(node) + setEditLabel(node.data('label')) + setModalOpen(true) + } + + // Function to close the modal + const handleCloseModal = () => { + setModalOpen(false) + } + + // Function to submit the new label + const handleSubmitModal = () => { + setModalOpen(false) + const nodeId = editNode.id() + const newText = editLabel + updateTextInMongoDB(nodeId, newText) + editNode.data('label', newText) + setEditNode(null) + } + useEffect(() => { // Fetch Tree data fetch(`/api/mongoDB/getTasks?projectID=${projectID}`, { @@ -56,24 +150,24 @@ function TreeGraph ({ className, projectID }) { }) // Tailwind's bg-gray-200 #E5E7EB - cytoscape({ - + const cy = cytoscape({ container: containerRef.current, elements, style: [ { selector: 'node', style: { - width: '1000', // Adjusts width based on label - height: '800', // Set a fixed height + shape: 'roundrectangle', + width: 1500, + height: 'label', // Use the 'label' keyword to dynamically size the width based on the label 'background-color': 'black', 'text-valign': 'center', label: 'data(label)', 'text-wrap': 'wrap', - 'text-max-width': 500, - padding: '30px', - color: 'white', // Tailwind's text-gray-900 - 'font-size': 100, + 'text-max-width': 1500, + padding: 75, + color: 'white', + 'font-size': 50, // Adjust font size as needed 'border-width': 10, 'border-color': '#FFFFFF' } @@ -81,8 +175,10 @@ function TreeGraph ({ className, projectID }) { { selector: 'edge', style: { - curveStyle: 'bezier', - 'line-color': 'white', // Tailwind's border-gray-300 #D1D5DB + // 'curve-style': 'unbundled-bezier', + // 'control-point-distances': 100, // Sharpness of the bend + // 'control-point-weights': 0.5, // Position of the control point along the edge (0.5 is halfway) + 'line-color': 'white', width: 10, targetArrowShape: 'triangle', 'target-arrow-color': 'white' // Tailwind's border-gray-300 @@ -90,17 +186,72 @@ function TreeGraph ({ className, projectID }) { } ], layout: { - name: 'breadthfirst', - spacingFactor: 1.3, - padding: 4, - directed: true - } + name: 'dagre', + spacingFactor: 1.5, + padding: 10, + directed: true, + avoidOverlap: true, + levelSpacing: 50, + rankDir: 'LR', + nodeSep: 50, + rankSep: 150 + }, + minZoom: 0.08, // Minimum zoom level (e.g., 0.5 means the graph can be zoomed out to half its original size) + maxZoom: 1 // Maximum zoom level (e.g., 2 means the graph can be zoomed in to twice its original size) + }) + + // creates context radial menu around each node + cy.cxtmenu({ + // adjust radius menu + menuRadius: function (ele) { return 70 }, + selector: 'node', + commands: [ + { + content: 'Add Child', + select: function (ele) { + console.log('Add Child clicked for node ' + ele.id()) + // Add logic to handle adding a child node + } + }, + { + content: 'Edit', + select: function (ele) { + handleEdit(ele) + } + }, + { + content: 'Delete', + select: function (ele) { + console.log('Delete clicked for node ' + ele.id()) + // Add logic to handle deleting the node + } + }, + { + content: 'Expand', + select: function (ele) { + console.log('Expand clicked for node ' + ele.id()) + // Add logic to handle expanding node + } + } + // ... [more commands as needed] + ] }) }, [tasks]) + const inputStyle = { + color: 'black', // Set font color to black + padding: '10px', // Optional: Add padding for better appearance + margin: '5px 0', // Optional: Add some margin for spacing + width: '100%' // Optional: Set width to fill the modal + // Add any other styles you need for the input + } + return (
+ + setEditLabel(e.target.value)} style={inputStyle} /> +
) } diff --git a/src/components/LeftSidebar.js b/src/components/LeftSidebar.js index 4fe461c..84c937b 100644 --- a/src/components/LeftSidebar.js +++ b/src/components/LeftSidebar.js @@ -10,7 +10,6 @@ import ContactUsButton from '../components/ContactUsButton' import Dropdown from '../components/Dropdown' import Button from '../components/Button' -import plusIcon from '../../public/svg/plus.svg' import starIcon from '../../public/svg/star.svg' import tableIcon from '../../public/svg/table.svg' import teamIcon from '../../public/svg/team.svg' @@ -71,19 +70,20 @@ export default function LeftSidebar ({ className }) {
    -
  • + + {/* To be implemented */} + {/*
  • */}
  • -
  • -
  • {projectsDiv}
  • +
-
+
diff --git a/src/components/Navbar.js b/src/components/Navbar.js index 3178406..386dd3a 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -5,6 +5,8 @@ import Image from 'next/image' import searchIcon from '../../public/svg/search.svg' import logo from '../../public/quaysideLogo.png' import { useSession, signOut } from 'next-auth/react' +import Button from '../components/Button' +import Link from 'next/link' /** * A Navbar component that fetches a user's name from a specified API and displays a navigation bar with several interactive elements. @@ -49,23 +51,28 @@ const Navbar = () => {
-
+
{/* Hamburger */} -
+ {/*
-
+
*/} {/* Logo */}
- quayside logo + quayside logo
+
+ + quayside.app +
{/* Current Directory */} + {/*
{ />
+ */} {/* Search Bar */}
+ +
+ {/* User */} +
{name}
+ {/* User Avatar Icon */} {session && session.user && ( -
+
)} - {/* User */} -
{name}
-