Skip to content

Commit f69b3aa

Browse files
authored
feat: Updated the HomePage with an img slider and project carousel (#20)
1 parent e23f2c5 commit f69b3aa

File tree

6 files changed

+383
-51
lines changed

6 files changed

+383
-51
lines changed

frontend/bitmatch/src/App.jsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import './styles/global.css';
1313
export default function App() {
1414
return (
1515
<Router>
16-
<div className="flex flex-col items-center justify-center min-h-screen">
16+
<div className="container mx-auto px-4 py-16 flex pb-6 flex-col items-center justify-center min-h-screen">
1717
<SignedOut>
1818
<div className="bg-white p-8 rounded-xl shadow-md text-center">
1919
<h1 className="text-3xl font-semibold text-gray-800 mb-4">
@@ -28,9 +28,13 @@ export default function App() {
2828
</SignedOut>
2929

3030
<SignedIn>
31-
<header className="w-full bg-white shadow-md p-4 fixed top-0 left-0 flex justify-between items-center z-10">
32-
<a href="/"><h2 className="font-sans text-xl font-black text-gray-800">BITMATCH</h2></a>
33-
<UserButton />
31+
<header className="w-full bg-white shadow-md p-4 fixed top-0 left-0 z-50">
32+
<div className="max-w-[1485px] mx-auto flex justify-between items-center">
33+
<a href="/">
34+
<h2 className="font-sans text-xl font-black text-gray-800">BITMATCH</h2>
35+
</a>
36+
<UserButton />
37+
</div>
3438
</header>
3539

3640
<Routes>

frontend/bitmatch/src/components/project/ProjectCardLarge.jsx

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,42 +16,32 @@ export default function ProjectCardLarge({ project, highlighted = false }) {
1616
: "border border-gray-200";
1717

1818
return (
19-
<div
20-
className={`flex flex-col ${borderClass} rounded-lg overflow-hidden transition-all duration-300 hover:border-blue-500 hover:shadow-lg`}
21-
>
19+
<div className={`flex flex-col h-full ${borderClass} rounded-lg overflow-hidden transition-all duration-300 hover:border-blue-500 hover:shadow-lg`}>
2220
{/* Header */}
2321
<div className="bg-gray-800 text-white p-2 flex justify-between">
2422
<span>{project.group}</span>
2523
</div>
2624

27-
{/* Image Placeholder with Match Percentage */}
25+
{/* Image Placeholder */}
2826
<div className="relative">
29-
{/* Conditional background color based on match percentage */}
27+
{/* Match Percentage */}
3028
<div
3129
className={`absolute top-2 left-2 text-xs px-2 py-1 rounded font-bold ${
32-
project.matchPercentage >= 90
33-
? "bg-green-700 text-white"
34-
: project.matchPercentage >= 80
35-
? "bg-green-600 text-white"
36-
: project.matchPercentage >= 70
37-
? "bg-yellow-600 text-white"
38-
: project.matchPercentage >= 60
39-
? "bg-yellow-500 text-white"
40-
: project.matchPercentage >= 50
41-
? "bg-yellow-400 text-white"
42-
: project.matchPercentage >= 40
43-
? "bg-orange-600 text-white"
44-
: project.matchPercentage >= 30
45-
? "bg-orange-500 text-white"
46-
: project.matchPercentage >= 20
47-
? "bg-red-600 text-white"
48-
: "bg-red-700 text-white"
30+
project.matchPercentage >= 90 ? "bg-green-700 text-white"
31+
: project.matchPercentage >= 80 ? "bg-green-600 text-white"
32+
: project.matchPercentage >= 70 ? "bg-yellow-600 text-white"
33+
: project.matchPercentage >= 60 ? "bg-yellow-500 text-white"
34+
: project.matchPercentage >= 50 ? "bg-yellow-400 text-white"
35+
: project.matchPercentage >= 40 ? "bg-orange-600 text-white"
36+
: project.matchPercentage >= 30 ? "bg-orange-500 text-white"
37+
: project.matchPercentage >= 20 ? "bg-red-600 text-white"
38+
: "bg-red-700 text-white"
4939
}`}
5040
>
5141
{project.matchPercentage}% Match
5242
</div>
5343

54-
{/* Use project.imageUrl to display the actual image */}
44+
{/* Image */}
5545
<div className="bg-gray-200 h-48 flex items-center justify-center text-gray-500 text-sm">
5646
{project.imageUrl ? (
5747
<img
@@ -65,8 +55,8 @@ export default function ProjectCardLarge({ project, highlighted = false }) {
6555
</div>
6656
</div>
6757

68-
{/* Content Section */}
69-
<div className="p-4 flex flex-col gap-2">
58+
{/* Content Wrapper */}
59+
<div className="p-4 flex flex-col flex-grow">
7060
<h3 className="font-bold text-lg">{project.title}</h3>
7161
<p className="text-sm text-gray-500">{project.institution}</p>
7262
<p className="text-sm">{project.description}</p>
@@ -87,7 +77,6 @@ export default function ProjectCardLarge({ project, highlighted = false }) {
8777
<div className="mt-2">
8878
<h4 className="font-bold text-sm uppercase">Top Positions Needed</h4>
8979
<div className="flex mt-1">
90-
{/* Split positions into two columns */}
9180
<ul className="w-1/2">
9281
{project.positions
9382
.slice(0, Math.ceil(project.positions.length / 2))
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"use client";
2+
3+
import { useState, useEffect, useCallback } from "react";
4+
import { ChevronLeft, ChevronRight } from "lucide-react";
5+
import { Button } from "@/components/ui/button";
6+
7+
export default function ImageSlideshow({ items }) {
8+
const [currentIndex, setCurrentIndex] = useState(0);
9+
const [isTransitioning, setIsTransitioning] = useState(false);
10+
11+
const goToSlide = useCallback(
12+
(slideIndex) => {
13+
if (isTransitioning) return;
14+
setIsTransitioning(true);
15+
setCurrentIndex(slideIndex);
16+
17+
setTimeout(() => {
18+
setIsTransitioning(false);
19+
}, 1000);
20+
},
21+
[isTransitioning]
22+
);
23+
24+
const goToPrevious = useCallback(() => {
25+
const newIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1;
26+
goToSlide(newIndex);
27+
}, [currentIndex, items.length, goToSlide]);
28+
29+
const goToNext = useCallback(() => {
30+
const newIndex = currentIndex === items.length - 1 ? 0 : currentIndex + 1;
31+
goToSlide(newIndex);
32+
}, [currentIndex, items.length, goToSlide]);
33+
34+
useEffect(() => {
35+
const slideInterval = setInterval(() => {
36+
if (!isTransitioning) {
37+
goToNext();
38+
}
39+
}, 5000);
40+
41+
return () => clearInterval(slideInterval);
42+
}, [goToNext, isTransitioning]);
43+
44+
if (!items || items.length === 0) return null;
45+
46+
const slideContentClasses = "absolute bottom-10 left-10 pl-20 pb-20 mr-40 z-10 text-left transition-transform duration-500 ease-in-out";
47+
const navButtonClasses = "absolute top-1/2 -translate-y-1/2 cursor-pointer z-20 hover:scale-110 transition-transform";
48+
49+
return (
50+
<a href={items[currentIndex]?.link} target="_blank" rel="noopener noreferrer" className="block">
51+
<div className="relative w-full h-[600px] bg-gray-200 overflow-hidden">
52+
<div className="h-full relative cursor-pointer">
53+
{items.map((slide, index) => (
54+
<div
55+
key={slide.id}
56+
className={`absolute top-0 left-0 w-full h-full z-0 transition-opacity duration-1000 ease-in-out ${
57+
index === currentIndex ? "opacity-100 z-10" : "opacity-0 z-0"
58+
}`}
59+
style={{
60+
backgroundImage: `url(${slide.image})`,
61+
backgroundSize: "cover",
62+
backgroundPosition: "center",
63+
}}
64+
>
65+
{/* Darker Overlay */}
66+
<div className="absolute inset-0 bg-black bg-opacity-50">
67+
<div className={`${slideContentClasses} ${index === currentIndex ? "translate-y-0 opacity-100 max-w-xl" : "translate-y-8 opacity-0"}`} >
68+
<h2 className="text-3xl font-bold text-white mb-3">{slide.title}</h2>
69+
<p className="mb-4 text-white">{slide.description}</p>
70+
<Button variant="outline" className="bg-white hover:bg-gray-100">
71+
View Project
72+
</Button>
73+
</div>
74+
</div>
75+
</div>
76+
))}
77+
</div>
78+
79+
{/* Left Arrow */}
80+
<div className={`${navButtonClasses} bg-white rounded-full p-1 shadow-md left-4`} onClick={(e) => { e.stopPropagation(); goToPrevious(); }}>
81+
<ChevronLeft size={36} className="text-black" />
82+
</div>
83+
84+
{/* Right Arrow */}
85+
<div className={`${navButtonClasses} bg-white rounded-full p-1 shadow-md right-4`} onClick={(e) => { e.stopPropagation(); goToNext(); }}>
86+
<ChevronRight size={36} className="text-black" />
87+
</div>
88+
89+
{/* Dots Navigation */}
90+
<div className="flex justify-center absolute bottom-4 left-0 right-0 z-20">
91+
{items.map((_, slideIndex) => (
92+
<div
93+
key={slideIndex}
94+
onClick={(e) => { e.stopPropagation(); goToSlide(slideIndex); }}
95+
className={`mx-1 w-3 h-3 rounded-full cursor-pointer transition-all duration-300 ${
96+
slideIndex === currentIndex ? "bg-white scale-110" : "border-2 border-white bg-gray-600 hover:bg-white"
97+
}`}
98+
></div>
99+
))}
100+
</div>
101+
</div>
102+
</a>
103+
);
104+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"use client";
2+
3+
import { useState, useRef, useEffect } from "react";
4+
import { ChevronLeft, ChevronRight } from "lucide-react";
5+
import ProjectCardLarge from "@/components/project/ProjectCardLarge";
6+
7+
export default function ProjectCarousel({ projects }) {
8+
const [scrollPosition, setScrollPosition] = useState(0);
9+
const [maxScroll, setMaxScroll] = useState(0);
10+
const [visibleCards, setVisibleCards] = useState(3);
11+
const carouselRef = useRef(null);
12+
13+
useEffect(() => {
14+
const handleResize = () => {
15+
if (window.innerWidth < 640) {
16+
setVisibleCards(1);
17+
} else if (window.innerWidth < 1024) {
18+
setVisibleCards(2);
19+
} else {
20+
setVisibleCards(3);
21+
}
22+
};
23+
24+
handleResize();
25+
window.addEventListener("resize", handleResize);
26+
27+
return () => {
28+
window.removeEventListener("resize", handleResize);
29+
};
30+
}, []);
31+
32+
useEffect(() => {
33+
if (carouselRef.current) {
34+
const containerWidth = carouselRef.current.clientWidth;
35+
const totalWidth = containerWidth * (projects.length / visibleCards);
36+
setMaxScroll(totalWidth - containerWidth);
37+
}
38+
}, [projects.length, visibleCards]);
39+
40+
const scrollLeft = () => {
41+
if (carouselRef.current) {
42+
const cardWidth = carouselRef.current.clientWidth / visibleCards;
43+
const newPosition = Math.max(scrollPosition - cardWidth, 0);
44+
setScrollPosition(newPosition);
45+
carouselRef.current.scrollTo({ left: newPosition, behavior: "smooth" });
46+
}
47+
};
48+
49+
const scrollRight = () => {
50+
if (carouselRef.current) {
51+
const cardWidth = carouselRef.current.clientWidth / visibleCards;
52+
const newPosition = Math.min(scrollPosition + cardWidth, maxScroll);
53+
setScrollPosition(newPosition);
54+
carouselRef.current.scrollTo({ left: newPosition, behavior: "smooth" });
55+
}
56+
};
57+
58+
return (
59+
<div className="relative">
60+
<button
61+
onClick={scrollLeft}
62+
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-4 z-10 bg-white rounded-full p-1 shadow-md"
63+
disabled={scrollPosition <= 0}
64+
>
65+
<ChevronLeft size={24} className={scrollPosition <= 0 ? "text-gray-300" : "text-black"} />
66+
</button>
67+
68+
<div
69+
ref={carouselRef}
70+
className="flex overflow-x-hidden scroll-smooth"
71+
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
72+
>
73+
{projects.map((project, index) => (
74+
<div
75+
key={project.id || `project-${index}`} // Ensures a unique key, even if `id` is missing
76+
className={`flex-shrink-0 px-1 ${
77+
visibleCards === 1 ? "w-full" : visibleCards === 2 ? "w-1/2" : "w-1/3"
78+
}`}
79+
>
80+
<ProjectCardLarge project={project} />
81+
</div>
82+
))}
83+
</div>
84+
85+
<button
86+
onClick={scrollRight}
87+
className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-4 z-10 bg-white rounded-full p-1 shadow-md"
88+
disabled={scrollPosition >= maxScroll}
89+
>
90+
<ChevronRight size={24} className={scrollPosition >= maxScroll ? "text-gray-300" : "text-black"} />
91+
</button>
92+
</div>
93+
);
94+
}

0 commit comments

Comments
 (0)