diff --git a/client/package-lock.json b/client/package-lock.json index 4512bc5..2882490 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@hookform/resolvers": "^3.9.0", "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", @@ -1134,6 +1135,33 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.2.tgz", + "integrity": "sha512-eGSlLzPhKO+TErxkiGcCZGuvbVMnLA1MTnyBksGOeGRGkxHiiJUujsjmNTdWTm4iHVSRaUao9/4Ur671auMghQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dialog": "1.1.2", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", diff --git a/client/package.json b/client/package.json index aca3282..50ed3df 100644 --- a/client/package.json +++ b/client/package.json @@ -12,6 +12,7 @@ "dependencies": { "@hookform/resolvers": "^3.9.0", "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", diff --git a/client/src/App.tsx b/client/src/App.tsx index 44a448c..168720e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -22,8 +22,8 @@ const App = () => { } /> } /> } /> - } /> - } /> + } /> + } /> 404} /> diff --git a/client/src/components/Projects/AddProject.tsx b/client/src/components/Projects/AddProject.tsx new file mode 100644 index 0000000..e69de29 diff --git a/client/src/components/Projects/ProjectCard.tsx b/client/src/components/Projects/ProjectCard.tsx new file mode 100644 index 0000000..d5e8bc4 --- /dev/null +++ b/client/src/components/Projects/ProjectCard.tsx @@ -0,0 +1,92 @@ +import { useState, useEffect } from "react"; +import { CircleIcon, StarIcon } from "@radix-ui/react-icons"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { FaStar as FilledStarIcon } from "react-icons/fa"; + +interface ProjectProps { + project: { + projectId: string; + title: string; + description: string; + repoLink: string; + starCount: number; + tags: string[]; + }; +} + +const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000'; + +export function ProjectCard({ project }: ProjectProps) { + const [isStarred, setIsStarred] = useState(false); + const [starCount, setStarCount] = useState(project.starCount); + const username = localStorage.getItem('devhub_username'); + + // Fetch initial star state from server/localStorage or logic to check if user has starred + useEffect(() => { + const fetchStarState = async () => { + const starredStatus = localStorage.getItem(`starred_${project.projectId}`); + setIsStarred(!!starredStatus); + }; + fetchStarState(); + }, [project.projectId]); + + const handleStarClick = async () => { + if (isStarred) return; // Prevent multiple stars + + try { + const response = await fetch(`${backendUrl}/profile/${username}/projects/${project.projectId}/star`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + }); + + if (response.ok) { + setStarCount((prevCount) => prevCount + 1); + setIsStarred(true); + localStorage.setItem(`starred_${project.projectId}`, "true"); + } else { + console.error("Failed to star the project"); + } + } catch (error) { + console.error("Error starring the project:", error); + } + }; + + return ( + + +
+ {project.title} + {project.description} +
+
+ +
+
+ +
+
+ + {project.tags.join(", ") || "No Tags"} +
+
+ + {starCount} Stars +
+
+
+
+ ); +} diff --git a/client/src/components/Projects/Projects.tsx b/client/src/components/Projects/Projects.tsx index 7ca44ec..8573aed 100644 --- a/client/src/components/Projects/Projects.tsx +++ b/client/src/components/Projects/Projects.tsx @@ -4,6 +4,20 @@ import { SidebarTrigger, } from "@/components/ui/sidebar" import { SidebarLeft } from '@/components/Sidebar/Sidebar' +import { useEffect, useState } from "react"; +import { ProjectCard } from "@/components/Projects/ProjectCard"; +import { useParams } from "react-router-dom"; + +const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000'; + +interface Project { + projectId: string; + title: string; + description: string; + repoLink: string; + starCount: number; + tags: string[]; +} const Projects = () => { return ( @@ -23,10 +37,53 @@ const Projects = () => { ) } +const fetchUserProjects = async (username: string) => { + const response = await fetch(`${backendUrl}/profile/${username}/projects`); + if (!response.ok) { + throw new Error("Failed to fetch projects"); + } + return response.json(); +}; + const Dashboard = () => { + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const { username } = useParams<{ username: string }>(); + + useEffect(() => { + const loadProjects = async () => { + try { + const data = await fetchUserProjects(username || ""); + setProjects(data.projects || []); + } catch (error) { + console.error("Error fetching projects:", error); + } finally { + setLoading(false); + } + }; + + if (username) { + loadProjects(); + } + }, [username]); + + if (loading) { + return
Loading projects...
; + } + + if (!projects.length) { + return
No projects found.
; + } return( <> - project +
+
+ {projects.map((project) => ( + + ))} +
+
+ ) } diff --git a/client/src/components/Sidebar/Sidebar.tsx b/client/src/components/Sidebar/Sidebar.tsx index 2f33bb5..90e112f 100644 --- a/client/src/components/Sidebar/Sidebar.tsx +++ b/client/src/components/Sidebar/Sidebar.tsx @@ -73,7 +73,7 @@ export function SidebarLeft({ ...props }: React.ComponentProps) }, { title: "Projects", - url: "/projects", + url: username ? `/projects/${username}` : "#", icon: Inbox, }, ], @@ -95,7 +95,7 @@ export function SidebarLeft({ ...props }: React.ComponentProps) }, { title: username ? `${username}` : "profile", - url: username ? `/u/${username}` : "#", + url: username ? `/user/${username}` : "#", icon: CircleUser, }, { diff --git a/client/src/components/ui/alert-dialog.tsx b/client/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..4791dce --- /dev/null +++ b/client/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/client/src/pages/Profile.tsx b/client/src/pages/Profile.tsx index f32a460..d0984be 100644 --- a/client/src/pages/Profile.tsx +++ b/client/src/pages/Profile.tsx @@ -323,7 +323,7 @@ const Dashboard = () => {
-

Projects

+ {/*

Projects

{profileData ? ( profileData.projects.map((project, index) => ( @@ -347,7 +347,7 @@ const Dashboard = () => { )} -
+
*/} {/* GitHub Stats */}
diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 1b2a485..0000000 --- a/package-lock.json +++ /dev/null @@ -1,425 +0,0 @@ -{ - "name": "devhub", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@tsparticles/engine": "^3.5.0", - "@tsparticles/react": "^3.0.0", - "@tsparticles/slim": "^3.5.0" - } - }, - "node_modules/@tsparticles/basic": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/basic/-/basic-3.5.0.tgz", - "integrity": "sha512-oty33TxM2aHWrzcwWRic1bQ04KBCdpnvzv8JXEkx5Uyp70vgVegUbtKmwGki3shqKZIt3v2qE4I8NsK6onhLrA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/matteobruni" - }, - { - "type": "github", - "url": "https://github.com/sponsors/tsparticles" - }, - { - "type": "buymeacoffee", - "url": "https://www.buymeacoffee.com/matteobruni" - } - ], - "dependencies": { - "@tsparticles/engine": "^3.5.0", - "@tsparticles/move-base": "^3.5.0", - "@tsparticles/shape-circle": "^3.5.0", - "@tsparticles/updater-color": "^3.5.0", - "@tsparticles/updater-opacity": "^3.5.0", - "@tsparticles/updater-out-modes": "^3.5.0", - "@tsparticles/updater-size": "^3.5.0" - } - }, - "node_modules/@tsparticles/engine": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/engine/-/engine-3.5.0.tgz", - "integrity": "sha512-RCwrJ2SvSYdhXJ24oUCjSUKEZQ9lXwObOWMvfMC9vS6/bk+Qo0N7Xx8AfumqzP/LebB1YJdlCvuoJMauAon0Pg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/matteobruni" - }, - { - "type": "github", - "url": "https://github.com/sponsors/tsparticles" - }, - { - "type": "buymeacoffee", - "url": "https://www.buymeacoffee.com/matteobruni" - } - ], - "hasInstallScript": true - }, - "node_modules/@tsparticles/interaction-external-attract": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-attract/-/interaction-external-attract-3.5.0.tgz", - "integrity": "sha512-BQYjoHtq7yaETSvPtzKt93OO9MZ1WuDZj7cFPG+iujNekXiwhLRQ89a+QMcsTrCLx70KLJ7SuTzQL5MT1/kb2Q==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/interaction-external-bounce": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-bounce/-/interaction-external-bounce-3.5.0.tgz", - "integrity": "sha512-H/0//dn4zwKes8zWIjolfeokL0VAlj+EkK7LUhznPhPu+Gt+h6aJqPlwC2MdI5Rvcnps8dT7YoCBWBQ4tJH6zg==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/interaction-external-bubble": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-bubble/-/interaction-external-bubble-3.5.0.tgz", - "integrity": "sha512-xTS4PQDMC5j9qOAFTC1M9DfBTJl8P8M41ySGtZHnCvVqG0oLlLSw15msniamjXyaoA4tZvBPM6G+GmFdgE9w1A==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/interaction-external-connect": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-connect/-/interaction-external-connect-3.5.0.tgz", - "integrity": "sha512-VSpyZ0P8Hu4nq6C917X3tnwEROfGjrm0ivWJmbBv/lFJ9euZ2VeezeITNZNtNvt/hS5vLI8npDetB/wtd994HQ==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/interaction-external-grab": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-grab/-/interaction-external-grab-3.5.0.tgz", - "integrity": "sha512-WOTWSGVerlfJZ9zwq8Eyutq1h0LAr1hI/Fs8j7s5qabZjxPzUBV8rhgghZ/nGrHEiB6j8SW4XMHkN6XR0VM9Ww==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/interaction-external-pause": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-pause/-/interaction-external-pause-3.5.0.tgz", - "integrity": "sha512-Hnj1mBH5X3d3zwTP6R+tYn45uTq5XGLDINhEzl30EAjXK30LQe8/RgE91O4CsMSrlTmouG0OuHYGC3nyrn/dcw==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/interaction-external-push": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-push/-/interaction-external-push-3.5.0.tgz", - "integrity": "sha512-8UNt5lYRhydDJCK7AznR3s1nJj3OCeLcDknARoq7hATdI+G151QAubD9NUUURCZ1GdXpftT5Bh0Bl1YtiZwOhg==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/interaction-external-remove": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-remove/-/interaction-external-remove-3.5.0.tgz", - "integrity": "sha512-+qiVRnR8xywg++gn8fagwpuQVh0WWKxIMkY6l6lMw9UoXz8L6MUVgvWaT632EVmkrTgM43pZ1m0W3m9aBY9rZw==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/interaction-external-repulse": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-repulse/-/interaction-external-repulse-3.5.0.tgz", - "integrity": "sha512-lTF7iLqCCQ3AbQSDVTpE3TihoVvI322/5QTqQmwylrrmjbDxGu4Hym4BHK5GqDHOdodOnwU2DWjRF5cRx3aPPg==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/interaction-external-slow": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-slow/-/interaction-external-slow-3.5.0.tgz", - "integrity": "sha512-KYp1GWbXdnLunFvHJt2YMZMMITebAt0XkzisKoSe+rfvoCbcMGXI2XvDYb0UkGvd8sKTSnHcn7cGH8dhVXXYaQ==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/interaction-particles-attract": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/interaction-particles-attract/-/interaction-particles-attract-3.5.0.tgz", - "integrity": "sha512-ICnT9+9ZxINh5ZijyxjFXOOMC/jNQgOXPe+5MxgK/WYXE8mRbRzsOdaxiS3zK5PSFlqtztn189anDbfqcHDutQ==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/interaction-particles-collisions": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/interaction-particles-collisions/-/interaction-particles-collisions-3.5.0.tgz", - "integrity": "sha512-KrfyXy4l6nW2z0An2FE4z5R4rEiIONYPcDpkBhWqQK+pyLkHhtGYmqmP7Pb22IC9noFzjOCaR5CNVjWP7B+1vA==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/interaction-particles-links": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/interaction-particles-links/-/interaction-particles-links-3.5.0.tgz", - "integrity": "sha512-ZdIixcGcRJMxCq4zxeRAzzbbuN5vVoy3pDDLaO3mnWnfJWywkYZToV0XvOUaHUT2AkUuKa9ZuQKx0LO3z1AO+w==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/move-base": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/move-base/-/move-base-3.5.0.tgz", - "integrity": "sha512-9oDk7zTxyhUCstj3lHTNTiWAgqIBzWa2o1tVQFK63Qwq+/WxzJCSwZOocC9PAHGM1IP6nA4zYJSfjbMBTrUocA==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/move-parallax": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/move-parallax/-/move-parallax-3.5.0.tgz", - "integrity": "sha512-1NC2OGsbdLc5T4uiRqq7i24b9FIhfaLnx4wVtOQjX48jWfy/ZKOdIEYLFKOPHnaMI0MjinJTNXLi9i6zVNCobg==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/plugin-easing-quad": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/plugin-easing-quad/-/plugin-easing-quad-3.5.0.tgz", - "integrity": "sha512-Pd44hTiVlaaeQZwRlP+ih8jKmWuIQdkpPUJS0Qwzeck2nfK01IAflDJoxXxGF53vA8QOrz/K6VdVQJShD8yCsg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/matteobruni" - }, - { - "type": "github", - "url": "https://github.com/sponsors/tsparticles" - }, - { - "type": "buymeacoffee", - "url": "https://www.buymeacoffee.com/matteobruni" - } - ], - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/react": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@tsparticles/react/-/react-3.0.0.tgz", - "integrity": "sha512-hjGEtTT1cwv6BcjL+GcVgH++KYs52bIuQGW3PWv7z3tMa8g0bd6RI/vWSLj7p//NZ3uTjEIeilYIUPBh7Jfq/Q==", - "peerDependencies": { - "@tsparticles/engine": "^3.0.2", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@tsparticles/shape-circle": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/shape-circle/-/shape-circle-3.5.0.tgz", - "integrity": "sha512-59TmXkeeI6Jzv5vt/D3TkclglabaoEXQi2kbDjSCBK68SXRHzlQu29mSAL41Y5S0Ft5ZJKkAQHX1IqEnm8Hyjg==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/shape-emoji": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/shape-emoji/-/shape-emoji-3.5.0.tgz", - "integrity": "sha512-cxWHxQxnG5vLDltkoxdo7KS87uKPwQgf4SDWy/WCxW4Psm1TEeeSGYMJPVed+wWPspOKmLb7u8OaEexgE2pHHQ==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/shape-image": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/shape-image/-/shape-image-3.5.0.tgz", - "integrity": "sha512-lWYg7DTv74dSOnXy+4dr7t1/OSuUmxDpIo12Lbxgx/QBN7A5I/HoqbKcs13TSA0RS1hcuMgtti07BcDTEYW3Dw==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/shape-line": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/shape-line/-/shape-line-3.5.0.tgz", - "integrity": "sha512-Qc4jwFEi/VnwmGwQBO/kCJEfNYdKHpeXfrdcqmm9c1B4iYHHDoaXJp6asMTggEfeAWu7fyPaO/7MURiPEqg7Hg==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/shape-polygon": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/shape-polygon/-/shape-polygon-3.5.0.tgz", - "integrity": "sha512-sqYL+YXpnq3nSWcOEGZaJ4Z7Cb7x8M0iORSLpPdNEIvwDKdPczYyQM95D8ep19Pv1CV5L0uRthV36wg7UpnJ9Q==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/shape-square": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/shape-square/-/shape-square-3.5.0.tgz", - "integrity": "sha512-rPHpA4Pzm1W5DIIow+lQS+VS7D2thSBQQbV9eHxb933Wh0/QC3me3w4vovuq7hdtVANhsUVO04n44Gc/2TgHkw==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/shape-star": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/shape-star/-/shape-star-3.5.0.tgz", - "integrity": "sha512-EDEJc4MYv3UbOeA3wrZjuJVtZ08PdCzzBij3T/7Tp3HUCf/p9XnfHBd/CPR5Mo6X0xpGfrein8UQN9CjGLHwUA==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/slim": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/slim/-/slim-3.5.0.tgz", - "integrity": "sha512-CKx3VtzsY0fs/dQc41PYtL3edm1z2sBROTgvz3adwqMyTWkQGnjLQhsM777Ebb6Yjf5Jxu4TzWOBc2HO7Cstkg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/matteobruni" - }, - { - "type": "github", - "url": "https://github.com/sponsors/tsparticles" - }, - { - "type": "buymeacoffee", - "url": "https://www.buymeacoffee.com/matteobruni" - } - ], - "dependencies": { - "@tsparticles/basic": "^3.5.0", - "@tsparticles/engine": "^3.5.0", - "@tsparticles/interaction-external-attract": "^3.5.0", - "@tsparticles/interaction-external-bounce": "^3.5.0", - "@tsparticles/interaction-external-bubble": "^3.5.0", - "@tsparticles/interaction-external-connect": "^3.5.0", - "@tsparticles/interaction-external-grab": "^3.5.0", - "@tsparticles/interaction-external-pause": "^3.5.0", - "@tsparticles/interaction-external-push": "^3.5.0", - "@tsparticles/interaction-external-remove": "^3.5.0", - "@tsparticles/interaction-external-repulse": "^3.5.0", - "@tsparticles/interaction-external-slow": "^3.5.0", - "@tsparticles/interaction-particles-attract": "^3.5.0", - "@tsparticles/interaction-particles-collisions": "^3.5.0", - "@tsparticles/interaction-particles-links": "^3.5.0", - "@tsparticles/move-parallax": "^3.5.0", - "@tsparticles/plugin-easing-quad": "^3.5.0", - "@tsparticles/shape-emoji": "^3.5.0", - "@tsparticles/shape-image": "^3.5.0", - "@tsparticles/shape-line": "^3.5.0", - "@tsparticles/shape-polygon": "^3.5.0", - "@tsparticles/shape-square": "^3.5.0", - "@tsparticles/shape-star": "^3.5.0", - "@tsparticles/updater-life": "^3.5.0", - "@tsparticles/updater-rotate": "^3.5.0", - "@tsparticles/updater-stroke-color": "^3.5.0" - } - }, - "node_modules/@tsparticles/updater-color": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/updater-color/-/updater-color-3.5.0.tgz", - "integrity": "sha512-TGGgiLixIg37sst2Fj9IV4XbdMwkT6PYanM7qEqyfmv4hJ/RHMQlCznEe6b7OhChQVBg5ov5EMl/BT4/fIWEYw==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/updater-life": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/updater-life/-/updater-life-3.5.0.tgz", - "integrity": "sha512-jlMEq16dwN+rZmW/UmLdqaCe4W0NFrVdmXkZV8QWYgu06a+Ucslz337nHYaP89/9rZWpNua/uq1JDjDzaVD5Jg==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/updater-opacity": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/updater-opacity/-/updater-opacity-3.5.0.tgz", - "integrity": "sha512-T2YfqdIFV/f5VOg1JQsXu6/owdi9g9K2wrJlBfgteo+IboVp6Lcuo4PGAfilWVkWrTdp1Nz4mz39NrLHfOce2g==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/updater-out-modes": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/updater-out-modes/-/updater-out-modes-3.5.0.tgz", - "integrity": "sha512-y6NZe2OSk5SrYdaLwUIQnHICsNEDIdPPJHQ2nAWSvAuPJphlSKjUknc7OaGiFwle6l0OkhWoZZe1rV1ktbw/lA==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/updater-rotate": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/updater-rotate/-/updater-rotate-3.5.0.tgz", - "integrity": "sha512-j4qPHQd1eUmDoGnIJOsVswHLqtTof1je+b2GTOLB3WIoKmlyUpzQYjVc7PNfLMuCEUubwpZCfcd/vC80VZeWkg==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/updater-size": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/updater-size/-/updater-size-3.5.0.tgz", - "integrity": "sha512-TnWlOChBsVZffT2uO0S4ALGSzxT6UAMIVlhGMGFgSeIlktKMqM+dxDGAPrYa1n8IS2dkVGisiXzsV0Ss6Ceu1A==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/@tsparticles/updater-stroke-color": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tsparticles/updater-stroke-color/-/updater-stroke-color-3.5.0.tgz", - "integrity": "sha512-29X1zER+W9IBDv0nTD/jRXnu5rEU7uv7+W1N0B6leBipjAY24sg7Kub2SvXAaBKz6kHHWuCeccBOwIiiTpDqMA==", - "dependencies": { - "@tsparticles/engine": "^3.5.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "peer": true - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "peer": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index ecee130..0000000 --- a/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "dependencies": { - "@tsparticles/engine": "^3.5.0", - "@tsparticles/react": "^3.0.0", - "@tsparticles/slim": "^3.5.0" - } -} diff --git a/server/api/handlers/projects/projects.py b/server/api/handlers/projects/projects.py index 204332d..3f9fc21 100644 --- a/server/api/handlers/projects/projects.py +++ b/server/api/handlers/projects/projects.py @@ -1,96 +1,172 @@ from flask import request, jsonify, current_app from models import Project from extensions import neo4j_db +import uuid + +def increment_star(username, project_id): + check_query = """ + MATCH (u:User {username: $username})-[:STARRED]->(p:Project {project_id: $project_id}) + RETURN p + """ + increment_star_query = """ + MATCH (u:User {username: $username}), (p:Project {project_id: $project_id}) + WHERE NOT (u)-[:STARRED]->(p) + SET p.star = p.star + 1 + CREATE (u)-[:STARRED]->(p) + RETURN p + """ + try: + with neo4j_db.driver.session() as session: + # Check if the user has already starred the project + check_result = session.run(check_query, username=username, project_id=project_id) + if check_result.single(): + return jsonify({'message': 'You have already starred this project'}), 400 + + # Increment the star count and create a STARRED relationship + result = session.run(increment_star_query, username=username, project_id=project_id) + project_record = result.single() + + if project_record: + project = project_record["p"] + return jsonify({ + 'message': 'Star count incremented successfully', + 'project': { + 'projectId': project["project_id"], + 'title': project["title"], + 'description': project.get("description", ""), + 'repoLink': project.get("repo_link", ""), + 'star': project.get("star") + } + }), 200 + else: + return jsonify({'message': 'Project not found'}), 404 + + except Exception as e: + current_app.logger.error(f"Error incrementing star count: {e}") + return jsonify({'message': 'An error occurred while incrementing the star count'}), 500 + +def get_projects(username): + query = """ + MATCH (u:User {username: $username})-[:OWNS]->(p:Project) + OPTIONAL MATCH (p)-[:TAGGED_WITH]->(t:Tag) + RETURN p, collect(t.name) AS tags + """ + try: + with neo4j_db.driver.session() as session: + result = session.run(query, username=username) + projects = [] + + for record in result: + project = record["p"] + tags = record["tags"] + projects.append({ + 'projectId': project.get('project_id'), + 'title': project.get('title'), + 'description': project.get('description', ''), + 'repoLink': project.get('repo_link', ''), + 'tags': tags if tags else [], + 'starCount' : project.get('star') + }) + + if projects: + return jsonify({'projects': projects}), 200 + else: + return jsonify({'message': 'No projects found for the user'}), 404 + + except Exception as e: + current_app.logger.error(f"Error fetching projects: {e}") + return jsonify({'message': 'An error occurred while fetching projects'}), 500 def add_project(username): data = request.get_json() - - # Get project details from request title = data.get('title') description = data.get('description', '') repo_link = data.get('repo_link', '') tags = data.get('tags', '') + project_id = str(uuid.uuid4()) + + domain_tags = [tag.strip() for tag in tags.split(',') if tag.strip()] - # Define a query to create a project without tags initially create_project_query = """ MATCH (u:User {username: $username}) - CREATE (p:Project {title: $title, description: $description, repo_link: $repo_link, tags: $tags}) + CREATE (p:Project {project_id: $project_id, title: $title, description: $description, repo_link: $repo_link, star: 0}) CREATE (u)-[:OWNS]->(p) RETURN p """ - try: with neo4j_db.driver.session() as session: - # Create the project - result = session.run(create_project_query, username=username, title=title, description=description, repo_link=repo_link, tags=tags) + result = session.run(create_project_query, username=username, project_id=project_id, title=title, description=description, repo_link=repo_link) project_record = result.single() if project_record: project = project_record["p"] - domain_tags = [tag for tag in tags.split(',') if tag.strip() in tags] - - # Update project with tags - update_project_query = """ - MATCH (p:Project {title: $title, description: $description, repo_link: $repo_link}) - WITH p, $tags AS tags - UNWIND tags AS tagName - MERGE (t:Tag {name: tagName}) - CREATE (p)-[:TAGGED_WITH]->(t) - RETURN p - """ + if domain_tags: + update_project_query = """ + MATCH (p:Project {project_id: $project_id}) + WITH p, $tags AS tags + UNWIND tags AS tagName + MERGE (t:Tag {name: tagName}) + CREATE (p)-[:TAGGED_WITH]->(t) + RETURN p + """ + session.run(update_project_query, project_id=project_id, tags=domain_tags) - # Update the project with tags - session.run(update_project_query, title=title, description=description, repo_link=repo_link, tags=domain_tags) - - return jsonify({'message': 'Project added and categorized successfully', 'project': { - 'title': project["title"], - 'description': project.get("description", ""), - 'repoLink': project.get("repo_link", ""), - 'tags': domain_tags - }}), 201 + return jsonify({ + 'message': 'Project added and categorized successfully', + 'project': { + 'projectId': project["project_id"], + 'title': project["title"], + 'description': project.get("description", ""), + 'repoLink': project.get("repo_link", ""), + 'tags': domain_tags, + 'star': 0 + } + }), 201 else: return jsonify({'message': 'User not found'}), 404 except Exception as e: current_app.logger.error(f"Error adding project: {e}") return jsonify({'message': 'An error occurred while adding the project'}), 500 - - def update_project(username, project_id): data = request.get_json() title = data.get('title') description = data.get('description') repo_link = data.get('repo_link') - tags = data.get('tags', '') + tags = [tag.strip() for tag in data.get('tags', '').split(',')] if data.get('tags') else [] query = """ - MATCH (u:User {username: $username})-[:OWNS]->(p:Project {id: $project_id}) + MATCH (u:User {username: $username})-[:OWNS]->(p:Project {project_id: $project_id}) SET p.title = $title, p.description = $description, p.repo_link = $repo_link WITH p OPTIONAL MATCH (p)-[r:TAGGED_WITH]->(t:Tag) DELETE r - WITH p - UNWIND $tags AS tagName + WITH p, $tags AS tags + UNWIND tags AS tagName MERGE (t:Tag {name: tagName}) - CREATE (p)-[:TAGGED_WITH]->(t) + MERGE (p)-[:TAGGED_WITH]->(t) // Change here to use MERGE for the relationship RETURN p """ - with neo4j_db.driver.session() as session: - result = session.run(query, username=username, project_id=project_id, title=title, description=description, repo_link=repo_link, tags=tags.split(', ') if tags else []) - if result.single(): - return jsonify({'message': 'Project updated successfully'}), 200 - return jsonify({'message': 'Project or user not found'}), 404 + try: + with neo4j_db.driver.session() as session: + result = session.run(query, username=username, project_id=project_id, title=title, description=description, repo_link=repo_link, tags=tags) + if result.single(): + return jsonify({'message': 'Project updated successfully'}), 200 + return jsonify({'message': 'Project or user not found'}), 404 + except Exception as e: + current_app.logger.error(f"Error updating project: {e}") + return jsonify({'message': 'An error occurred while updating the project'}), 500 -def delete_project(username, project_title): +def delete_project(username, project_id): query = """ - MATCH (u:User {username: $username})-[:OWNS]->(p:Project {title: $project_title}) + MATCH (u:User {username: $username})-[:OWNS]->(p:Project {project_id: $project_id}) OPTIONAL MATCH (p)-[r]-() DELETE r, p RETURN u """ with neo4j_db.driver.session() as session: - result = session.run(query, username=username, project_title=project_title) + result = session.run(query, username=username, project_id=project_id) if result.single(): return jsonify({'message': 'Project deleted successfully'}), 200 - return jsonify({'message': 'Project or user not found'}), 404 \ No newline at end of file + return jsonify({'message': 'Project or user not found'}), 404 diff --git a/server/api/urls.py b/server/api/urls.py index ce72174..5f3614e 100644 --- a/server/api/urls.py +++ b/server/api/urls.py @@ -1,6 +1,6 @@ from api.handlers.auth.userauth import signup, login, check_auth, home, logout, index, check_username from api.handlers.user.profile import get_profile, update_profile -from api.handlers.projects.projects import add_project, update_project, delete_project +from api.handlers.projects.projects import add_project, update_project, delete_project, get_projects, increment_star from api.handlers.analyze.githubdata import github_data, top_languages, streak_stats, pinned_repos, streak_chart from api.handlers.analyze.leetcodedata import leetcode_data, leetcode_card from api.handlers.query.querymodel import chat,chat_history @@ -34,9 +34,11 @@ def register_routes(app): app.add_url_rule('/analyze/leetcode_card', 'leetcode_card', leetcode_card, methods=['POST']) # Project routes + app.add_url_rule('/profile//projects', 'get_projects', get_projects, methods=['GET']) app.add_url_rule('/profile//projects', 'add_project', add_project, methods=['POST']) - app.add_url_rule('/profile//projects/', 'update_project', update_project, methods=['PUT']) - app.add_url_rule('/profile//projects/', 'delete_project', delete_project, methods=['DELETE']) + app.add_url_rule('/profile//projects/', 'update_project', update_project, methods=['PUT']) + app.add_url_rule('/profile//projects/', 'delete_project', delete_project, methods=['DELETE']) + app.add_url_rule('/profile//projects//star', 'increment_star', increment_star, methods=['POST']) # Chat with model routes app.add_url_rule('/chat', 'chat', chat, methods=['POST']) diff --git a/server/models.py b/server/models.py index a824088..fafebd6 100644 --- a/server/models.py +++ b/server/models.py @@ -61,21 +61,24 @@ def remove_friend(self, friend_user): class Project(Base): __tablename__ = 'projects' - - id = Column(Integer, primary_key=True) + + project_id = Column(Integer, primary_key=True) title = Column(String, nullable=False) description = Column(Text, nullable=True) repo_link = Column(String, nullable=True) + star = Column(Integer, default=0, nullable=False) user_id = Column(Integer, ForeignKey('users.id')) user = relationship('User', back_populates='projects') tags = relationship('Tag', secondary=project_tags, back_populates='projects') - def __init__(self, title, description=None, repo_link=None): + def __init__(self, title, description=None, repo_link=None, star=0): self.title = title self.description = description self.repo_link = repo_link + self.star = star + class Tag(Base): __tablename__ = 'tags'