Skip to content

Commit c380192

Browse files
authored
Merge pull request #64 from deepraj21/main
feat: added star to project
2 parents bbe9a7a + 22b4913 commit c380192

File tree

14 files changed

+454
-488
lines changed

14 files changed

+454
-488
lines changed

client/package-lock.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"dependencies": {
1313
"@hookform/resolvers": "^3.9.0",
1414
"@radix-ui/react-accordion": "^1.2.0",
15+
"@radix-ui/react-alert-dialog": "^1.1.2",
1516
"@radix-ui/react-avatar": "^1.1.1",
1617
"@radix-ui/react-collapsible": "^1.1.1",
1718
"@radix-ui/react-dialog": "^1.1.2",

client/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ const App = () => {
2222
<Route path="/home" element={<Home />} />
2323
<Route path="/settings" element={<EditProfileForm />} />
2424
<Route path="/message" element={<MessagePage/>} />
25-
<Route path="/projects" element={<Projects />} />
26-
<Route path="/u/:username" element={<Profile />} />
25+
<Route path="/projects/:username" element={<Projects />} />
26+
<Route path="/user/:username" element={<Profile />} />
2727
<Route path="*" element={<div>404</div>} />
2828
</Routes>
2929
</Router>

client/src/components/Projects/AddProject.tsx

Whitespace-only changes.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { useState, useEffect } from "react";
2+
import { CircleIcon, StarIcon } from "@radix-ui/react-icons";
3+
import { Button } from "@/components/ui/button";
4+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5+
import { FaStar as FilledStarIcon } from "react-icons/fa";
6+
7+
interface ProjectProps {
8+
project: {
9+
projectId: string;
10+
title: string;
11+
description: string;
12+
repoLink: string;
13+
starCount: number;
14+
tags: string[];
15+
};
16+
}
17+
18+
const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000';
19+
20+
export function ProjectCard({ project }: ProjectProps) {
21+
const [isStarred, setIsStarred] = useState(false);
22+
const [starCount, setStarCount] = useState(project.starCount);
23+
const username = localStorage.getItem('devhub_username');
24+
25+
// Fetch initial star state from server/localStorage or logic to check if user has starred
26+
useEffect(() => {
27+
const fetchStarState = async () => {
28+
const starredStatus = localStorage.getItem(`starred_${project.projectId}`);
29+
setIsStarred(!!starredStatus);
30+
};
31+
fetchStarState();
32+
}, [project.projectId]);
33+
34+
const handleStarClick = async () => {
35+
if (isStarred) return; // Prevent multiple stars
36+
37+
try {
38+
const response = await fetch(`${backendUrl}/profile/${username}/projects/${project.projectId}/star`, {
39+
method: "POST",
40+
headers: { "Content-Type": "application/json" },
41+
});
42+
43+
if (response.ok) {
44+
setStarCount((prevCount) => prevCount + 1);
45+
setIsStarred(true);
46+
localStorage.setItem(`starred_${project.projectId}`, "true");
47+
} else {
48+
console.error("Failed to star the project");
49+
}
50+
} catch (error) {
51+
console.error("Error starring the project:", error);
52+
}
53+
};
54+
55+
return (
56+
<Card>
57+
<CardHeader className="grid grid-cols-[1fr_110px] items-start gap-4 space-y-0">
58+
<div className="space-y-1">
59+
<CardTitle>{project.title}</CardTitle>
60+
<CardDescription>{project.description}</CardDescription>
61+
</div>
62+
<div className="flex items-center rounded-md bg-secondary text-secondary-foreground">
63+
<Button
64+
variant="secondary"
65+
className=" shadow-none"
66+
onClick={handleStarClick}
67+
disabled={isStarred}
68+
>
69+
{isStarred ? (
70+
<FilledStarIcon className="mr-2 h-4 w-4 text-yellow-400" />
71+
) : (
72+
<StarIcon className="mr-2 h-4 w-4" />
73+
)}
74+
Star
75+
</Button>
76+
</div>
77+
</CardHeader>
78+
<CardContent>
79+
<div className="flex space-x-4 text-sm text-muted-foreground">
80+
<div className="flex items-center">
81+
<CircleIcon className="mr-1 h-3 w-3 fill-sky-400 text-sky-400" />
82+
{project.tags.join(", ") || "No Tags"}
83+
</div>
84+
<div className="flex items-center">
85+
<FilledStarIcon className="mr-1 h-3 w-3 text-yellow-400" />
86+
{starCount} Stars
87+
</div>
88+
</div>
89+
</CardContent>
90+
</Card>
91+
);
92+
}

client/src/components/Projects/Projects.tsx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ import {
44
SidebarTrigger,
55
} from "@/components/ui/sidebar"
66
import { SidebarLeft } from '@/components/Sidebar/Sidebar'
7+
import { useEffect, useState } from "react";
8+
import { ProjectCard } from "@/components/Projects/ProjectCard";
9+
import { useParams } from "react-router-dom";
10+
11+
const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000';
12+
13+
interface Project {
14+
projectId: string;
15+
title: string;
16+
description: string;
17+
repoLink: string;
18+
starCount: number;
19+
tags: string[];
20+
}
721

822
const Projects = () => {
923
return (
@@ -23,10 +37,53 @@ const Projects = () => {
2337
)
2438
}
2539

40+
const fetchUserProjects = async (username: string) => {
41+
const response = await fetch(`${backendUrl}/profile/${username}/projects`);
42+
if (!response.ok) {
43+
throw new Error("Failed to fetch projects");
44+
}
45+
return response.json();
46+
};
47+
2648
const Dashboard = () => {
49+
const [projects, setProjects] = useState<Project[]>([]);
50+
const [loading, setLoading] = useState(true);
51+
const { username } = useParams<{ username: string }>();
52+
53+
useEffect(() => {
54+
const loadProjects = async () => {
55+
try {
56+
const data = await fetchUserProjects(username || "");
57+
setProjects(data.projects || []);
58+
} catch (error) {
59+
console.error("Error fetching projects:", error);
60+
} finally {
61+
setLoading(false);
62+
}
63+
};
64+
65+
if (username) {
66+
loadProjects();
67+
}
68+
}, [username]);
69+
70+
if (loading) {
71+
return <div>Loading projects...</div>;
72+
}
73+
74+
if (!projects.length) {
75+
return <div>No projects found.</div>;
76+
}
2777
return(
2878
<>
29-
project
79+
<div className="flex flex-1 flex-col gap-4 p-4">
80+
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
81+
{projects.map((project) => (
82+
<ProjectCard key={project.projectId} project={project} />
83+
))}
84+
</div>
85+
</div>
86+
3087
</>
3188
)
3289
}

client/src/components/Sidebar/Sidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function SidebarLeft({ ...props }: React.ComponentProps<typeof Sidebar>)
7373
},
7474
{
7575
title: "Projects",
76-
url: "/projects",
76+
url: username ? `/projects/${username}` : "#",
7777
icon: Inbox,
7878
},
7979
],
@@ -95,7 +95,7 @@ export function SidebarLeft({ ...props }: React.ComponentProps<typeof Sidebar>)
9595
},
9696
{
9797
title: username ? `${username}` : "profile",
98-
url: username ? `/u/${username}` : "#",
98+
url: username ? `/user/${username}` : "#",
9999
icon: CircleUser,
100100
},
101101
{
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import * as React from "react"
2+
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3+
4+
import { cn } from "@/lib/utils"
5+
import { buttonVariants } from "@/components/ui/button"
6+
7+
const AlertDialog = AlertDialogPrimitive.Root
8+
9+
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
10+
11+
const AlertDialogPortal = AlertDialogPrimitive.Portal
12+
13+
const AlertDialogOverlay = React.forwardRef<
14+
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
15+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
16+
>(({ className, ...props }, ref) => (
17+
<AlertDialogPrimitive.Overlay
18+
className={cn(
19+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
20+
className
21+
)}
22+
{...props}
23+
ref={ref}
24+
/>
25+
))
26+
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
27+
28+
const AlertDialogContent = React.forwardRef<
29+
React.ElementRef<typeof AlertDialogPrimitive.Content>,
30+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
31+
>(({ className, ...props }, ref) => (
32+
<AlertDialogPortal>
33+
<AlertDialogOverlay />
34+
<AlertDialogPrimitive.Content
35+
ref={ref}
36+
className={cn(
37+
"fixed left-[50%] top-[50%] z-50 grid w-[90%] sm:w-auto translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
38+
className
39+
)}
40+
{...props}
41+
/>
42+
</AlertDialogPortal>
43+
))
44+
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
45+
46+
const AlertDialogHeader = ({
47+
className,
48+
...props
49+
}: React.HTMLAttributes<HTMLDivElement>) => (
50+
<div
51+
className={cn(
52+
"flex flex-col space-y-2 text-center sm:text-left",
53+
className
54+
)}
55+
{...props}
56+
/>
57+
)
58+
AlertDialogHeader.displayName = "AlertDialogHeader"
59+
60+
const AlertDialogFooter = ({
61+
className,
62+
...props
63+
}: React.HTMLAttributes<HTMLDivElement>) => (
64+
<div
65+
className={cn(
66+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
67+
className
68+
)}
69+
{...props}
70+
/>
71+
)
72+
AlertDialogFooter.displayName = "AlertDialogFooter"
73+
74+
const AlertDialogTitle = React.forwardRef<
75+
React.ElementRef<typeof AlertDialogPrimitive.Title>,
76+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
77+
>(({ className, ...props }, ref) => (
78+
<AlertDialogPrimitive.Title
79+
ref={ref}
80+
className={cn("text-lg font-semibold", className)}
81+
{...props}
82+
/>
83+
))
84+
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
85+
86+
const AlertDialogDescription = React.forwardRef<
87+
React.ElementRef<typeof AlertDialogPrimitive.Description>,
88+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
89+
>(({ className, ...props }, ref) => (
90+
<AlertDialogPrimitive.Description
91+
ref={ref}
92+
className={cn("text-sm text-muted-foreground", className)}
93+
{...props}
94+
/>
95+
))
96+
AlertDialogDescription.displayName =
97+
AlertDialogPrimitive.Description.displayName
98+
99+
const AlertDialogAction = React.forwardRef<
100+
React.ElementRef<typeof AlertDialogPrimitive.Action>,
101+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
102+
>(({ className, ...props }, ref) => (
103+
<AlertDialogPrimitive.Action
104+
ref={ref}
105+
className={cn(buttonVariants(), className)}
106+
{...props}
107+
/>
108+
))
109+
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
110+
111+
const AlertDialogCancel = React.forwardRef<
112+
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
113+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
114+
>(({ className, ...props }, ref) => (
115+
<AlertDialogPrimitive.Cancel
116+
ref={ref}
117+
className={cn(
118+
buttonVariants({ variant: "outline" }),
119+
"mt-2 sm:mt-0",
120+
className
121+
)}
122+
{...props}
123+
/>
124+
))
125+
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
126+
127+
export {
128+
AlertDialog,
129+
AlertDialogPortal,
130+
AlertDialogOverlay,
131+
AlertDialogTrigger,
132+
AlertDialogContent,
133+
AlertDialogHeader,
134+
AlertDialogFooter,
135+
AlertDialogTitle,
136+
AlertDialogDescription,
137+
AlertDialogAction,
138+
AlertDialogCancel,
139+
}

0 commit comments

Comments
 (0)