Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/(user)/projects/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
</div>
{project.contributors.length > 0 && (
<div className="flex items-center gap-2 text-muted-foreground">
<Users className="h-4 w-4" />
<Users className="h-6 w-6" />
<span className="text-sm font-medium">
{project.contributors.length}
</span>
Expand Down
27 changes: 26 additions & 1 deletion src/app/api/projects/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,25 @@ export async function GET(req: Request) {
];
}

const session = await getServerSession(authOptions);
const userId = session?.user?.id;

let userLikes: string[] = [];
if (userId) {
const userProfile = await db.profile.findUnique({
where: { userId },
select: { id: true }
});

if (userProfile) {
const likes = await db.like.findMany({
where: { profileId: userProfile.id },
select: { projectId: true }
});
userLikes = likes.map(like => like.projectId);
}
}

const projects = await db.project.findMany({
skip: (page - 1) * limit,
take: limit,
Expand Down Expand Up @@ -103,8 +122,14 @@ export async function GET(req: Request) {
},
});

const projectsWithLikes = projects.map(project => ({
...project,
isLiked: userLikes.includes(project.id)
}));


// Custom sort to ensure Active > Planning > Completed
const sortedProjects = projects.sort((a, b) => {
const sortedProjects = projectsWithLikes.sort((a, b) => {
const statusOrder: Record<string, number> = {
'Active': 1,
'Planning': 2,
Expand Down
48 changes: 23 additions & 25 deletions src/components/LikeButton.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"use client";

import { useState } from 'react';
import { FaHeart, FaRegHeart } from 'react-icons/fa';
import { useToast } from '@/components/ui/toast-1';
import { clsx } from 'clsx';
import { useSession } from 'next-auth/react';
import { LoginModal } from './LoginModal';
import { useState } from "react";
import { FaHeart, FaRegHeart } from "react-icons/fa";
import { useToast } from "@/components/ui/toast-1";
import { clsx } from "clsx";
import { useSession } from "next-auth/react";
import { LoginModal } from "./LoginModal";

interface LikeButtonProps {
projectId: string;
isInitiallyLiked: boolean;
initialLikeCount: number;
initialLikeCount: number;
disabled?: boolean;
}

Expand All @@ -37,35 +37,35 @@ const LikeButton = ({
if (isLoading || disabled) return;

setIsLoading(true);

// --- 1. STORE PREVIOUS STATE FOR ROLLBACK ---
const previousLikeState = isLiked;
const previousLikeCount = likeCount;

// --- 2. OPTIMISTIC UPDATE ---
// Update both the icon and the count immediately
setIsLiked(!previousLikeState);
setLikeCount(previousLikeState ? previousLikeCount - 1 : previousLikeCount + 1);
setLikeCount(
previousLikeState ? previousLikeCount - 1 : previousLikeCount + 1
);

try {
// 3. API CALL (same as before)
const response = await fetch(`/api/projects/${projectId}/like`, {
method: 'POST',
method: "POST",
});

if (!response.ok) {
throw new Error('Failed to update like status');
throw new Error("Failed to update like status");
}

} catch (error) {
console.error(error);

// --- 4. ROLLBACK ON ERROR ---
// If the API call fails, revert both states
setIsLiked(previousLikeState);
setLikeCount(previousLikeCount);
showToast('Failed to update like status', 'error');

showToast("Failed to update like status", "error");
} finally {
setIsLoading(false);
}
Expand All @@ -82,29 +82,27 @@ const LikeButton = ({
onClick={toggleLike}
disabled={isDisabled}
className={clsx(
'inline-flex items-center justify-center transition-opacity h-4 w-4', // Added h-4 w-4
"inline-flex items-center justify-center transition-opacity h-6 w-6",
{
'cursor-not-allowed opacity-70': isDisabled,
'cursor-pointer': !isDisabled,
"cursor-not-allowed opacity-70": isDisabled,
"cursor-pointer": !isDisabled,
}
)}
aria-label={
disabled ? 'Login to like' : isLiked ? 'Unlike' : 'Like'
}
aria-label={disabled ? "Login to like" : isLiked ? "Unlike" : "Like"}
>
{isLiked ? (
<FaHeart className="text-red-500" />
<FaHeart className="text-red-500 w-full h-full" />
) : (
<FaRegHeart className="text-current" />
<FaRegHeart className="text-current w-full h-full" />
)}
</button>

{/* Render the likeCount state, which updates instantly */}
<span className="text-sm font-medium">{likeCount}</span>
<span className="text-lg font-medium">{likeCount}</span>
</div>
<LoginModal open={loginOpen} onClose={() => setLoginOpen(false)} />
</>
);
};

export default LikeButton;
export default LikeButton;
5 changes: 4 additions & 1 deletion src/components/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface Project {
projectStatus: "Planning" | "Active" | "Completed";
isActive: boolean;
authorId: string;
isLiked: boolean;
author: {
id: string;
name: string;
Expand Down Expand Up @@ -136,11 +137,13 @@ export default function ProjectCard() {
ref={index === projects.length - 1 ? lastProjectRef : null}
>
<ProjectShowcaseCard
projectId={project.id}
title={project.title}
tagline={`By ${project.author.name}`}
description={project.description}
status={project.projectStatus}
likes={project._count.likes}
initialLikeCount={project._count.likes}
isInitiallyLiked={project.isLiked}
href={`/projects/${project.id}` || "#"}
githubUrl={project.githubLink || undefined}
onViewProject={handleViewProject}
Expand Down
102 changes: 54 additions & 48 deletions src/components/ui/card-7.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
CheckCircle2,
Github,
} from "lucide-react";
import LikeButton from "../LikeButton";

// Status badge component (Left as-is, these are semantic state colors)
const StatusBadge = ({
Expand Down Expand Up @@ -78,11 +79,13 @@ interface TechStackItem {

// Define the props for the ProjectCard component
interface ProjectCardProps extends React.HTMLAttributes<HTMLDivElement> {
projectId: string; // Add projectId here
title: string;
tagline: string;
description: string;
status: "Planning" | "Active" | "Completed";
likes: number;
initialLikeCount: number;
isInitiallyLiked: boolean;
// comments: number;
href: string;
githubUrl?: string; // Optional GitHub URL
Expand All @@ -99,13 +102,15 @@ const ProjectCard = React.forwardRef<HTMLDivElement, ProjectCardProps>(
tagline,
description,
status,
likes,
initialLikeCount,
isInitiallyLiked,
// comments,
href,
githubUrl,
techStack = [],
isAuthenticated,
onViewProject,
projectId, // Destructure projectId to prevent it from being passed to the div
...props
},
ref
Expand All @@ -121,18 +126,15 @@ const ProjectCard = React.forwardRef<HTMLDivElement, ProjectCardProps>(
};

checkMobile();
window.addEventListener('resize', checkMobile);
window.addEventListener("resize", checkMobile);

return () => window.removeEventListener('resize', checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}, []);

// Toggle expansion on mobile
const handleCardClick = (e: React.MouseEvent) => {
// Don't toggle if clicking on links or buttons
if (
isMobile &&
!(e.target as HTMLElement).closest('a, button')
) {
if (isMobile && !(e.target as HTMLElement).closest("a, button")) {
setIsExpanded(!isExpanded);
}
};
Expand Down Expand Up @@ -162,7 +164,6 @@ const ProjectCard = React.forwardRef<HTMLDivElement, ProjectCardProps>(
>
{/* Content Container */}
<div className="relative flex h-full flex-col p-6 text-card-foreground">

{/* Top Section: Status Badge and GitHub Link */}
<div className="flex items-start justify-between mb-6">
<StatusBadge status={status} />
Expand Down Expand Up @@ -240,68 +241,73 @@ const ProjectCard = React.forwardRef<HTMLDivElement, ProjectCardProps>(
</Button>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<Heart
className="h-5 w-5 text-destructive fill-destructive" // Use destructive red
<LikeButton
projectId={projectId}
isInitiallyLiked={isInitiallyLiked}
initialLikeCount={initialLikeCount}
/>
<span className="text-lg font-bold text-foreground">
{likes}
</span>
</div>
</div>
</div>

{/* Tech Stack Section (shown on hover for desktop, on click for mobile) */}
{techStack.length > 0 && (
<div className={cn(
"transition-all duration-300 ease-in-out overflow-hidden",
!isMobile && "opacity-0 max-h-0 group-hover:opacity-100 group-hover:max-h-96 group-hover:mt-8 group-hover:mb-6",
isMobile && (isExpanded ? "opacity-100 max-h-96 mt-8 mb-6" : "opacity-0 max-h-0")
)}>
<div
className={cn(
"transition-all duration-300 ease-in-out overflow-hidden",
!isMobile &&
"opacity-0 max-h-0 group-hover:opacity-100 group-hover:max-h-96 group-hover:mt-8 group-hover:mb-6",
isMobile &&
(isExpanded
? "opacity-100 max-h-96 mt-8 mb-6"
: "opacity-0 max-h-0")
)}
>
<h4 className="font-semibold text-foreground mb-2 text-xs tracking-wide">
TECH STACK
</h4>
<div className="flex flex-wrap gap-3">
{techStack.filter(tech => tech && tech.name).map((tech, index) => (
<div
key={index}
className={cn(
"group/tech flex items-center gap-2 px-3 py-2 rounded-lg",
"bg-muted border border-border", // Use muted background
"hover:bg-accent hover:border-input", // Use accent/input for hover
"transition-all duration-200"
)}
title={tech.name}
>
{techStack
.filter((tech) => tech && tech.name)
.map((tech, index) => (
<div
key={index}
className={cn(
"w-5 h-5 flex items-center justify-center",
"text-muted-foreground group-hover/tech:text-foreground",
"transition-colors"
"group/tech flex items-center gap-2 px-3 py-2 rounded-lg",
"bg-muted border border-border",
"hover:bg-accent hover:border-input",
"transition-all duration-200"
)}
title={tech.name}
>
{tech?.icon || null}
<div
className={cn(
"w-5 h-5 flex items-center justify-center",
"text-muted-foreground group-hover/tech:text-foreground",
"transition-colors"
)}
>
{tech?.icon || null}
</div>
<span
className={cn(
"text-xs font-medium",
"text-muted-foreground group-hover/tech:text-foreground",
"transition-colors"
)}
>
{tech.name}
</span>
</div>
<span
className={cn(
"text-xs font-medium",
"text-muted-foreground group-hover/tech:text-foreground",
"transition-colors"
)}
>
{tech.name}
</span>
</div>
))}
))}
</div>
</div>
)}


</div>
</div>
);
}
);
ProjectCard.displayName = "ProjectCard";

export { ProjectCard };
export { ProjectCard };