Skip to content

Commit e8545ad

Browse files
authored
feat: I built the explore section of the landing page (#276)
1 parent 721febe commit e8545ad

10 files changed

Lines changed: 390 additions & 22 deletions

File tree

app/(landing)/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import BackedBy from '@/components/landing-page/BackedBy';
88
import NewsLetter from '@/components/landing-page/NewsLetter';
99
import BlogSection from '@/components/landing-page/blog/BlogSection';
1010
import { Footer } from '@/components/landing-page/footer';
11+
import Explore from '@/components/landing-page/Explore';
1112

1213
export default function LandingPage() {
1314
return (
1415
<div className='relative overflow-hidden'>
1516
<BeamBackground />
1617
<div className='relative z-10 mx-auto max-w-[1300px] space-y-[60px] md:space-y-[80px]'>
1718
<Hero />
19+
<Explore />
1820
<WhyBoundless />
1921
<BackedBy />
2022
<NewsLetter />
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
'use client';
2+
3+
import { cn } from '@/lib/utils';
4+
import { ArrowRight } from 'lucide-react';
5+
import Image from 'next/image';
6+
import { useState, useRef, useEffect } from 'react';
7+
8+
interface Projects {
9+
id: string;
10+
daysToDeadline?: number;
11+
status: string;
12+
projectImg: string;
13+
currentAmount?: number;
14+
targetAmount?: number;
15+
currentVotes?: number;
16+
targetVotes?: number;
17+
milestonesCompleted?: number;
18+
totalMilestones?: number;
19+
milestonesRejected?: number;
20+
}
21+
22+
const projects: Projects[] = [
23+
{
24+
id: '1',
25+
status: 'Validation',
26+
currentVotes: 28,
27+
targetVotes: 100,
28+
daysToDeadline: 43,
29+
projectImg: '/landing/explore/project-placeholder-1.png',
30+
},
31+
{
32+
id: '2',
33+
status: 'Funding',
34+
currentAmount: 120000,
35+
targetAmount: 300000,
36+
daysToDeadline: 15,
37+
projectImg: '/landing/explore/project-placeholder-2.png',
38+
},
39+
{
40+
id: '3',
41+
status: 'Completed',
42+
milestonesCompleted: 6,
43+
totalMilestones: 6,
44+
milestonesRejected: 1,
45+
projectImg: '/landing/explore/project-placeholder-3.png',
46+
},
47+
{
48+
id: '4',
49+
status: 'Funded',
50+
milestonesCompleted: 3,
51+
totalMilestones: 6,
52+
daysToDeadline: 3,
53+
projectImg: '/landing/explore/project-placeholder-4.png',
54+
},
55+
{
56+
id: '5',
57+
status: 'Funding',
58+
currentAmount: 120000,
59+
targetAmount: 300000,
60+
daysToDeadline: 15,
61+
projectImg: '/landing/explore/project-placeholder-2.png',
62+
},
63+
{
64+
id: '6',
65+
status: 'Validation',
66+
currentVotes: 28,
67+
targetVotes: 100,
68+
daysToDeadline: 43,
69+
projectImg: '/landing/explore/project-placeholder-1.png',
70+
},
71+
];
72+
73+
const tabs = [
74+
{ name: 'Featured Projects', value: 'featured-projects' },
75+
{ name: 'Ongoing Hackathons', value: 'ongoing-hackathons' },
76+
{ name: 'Open Grants', value: 'open-grants' },
77+
];
78+
79+
export default function Explore() {
80+
const [activeTab, setActiveTab] = useState(tabs[0].value);
81+
const [underlineStyle, setUnderlineStyle] = useState({});
82+
const tabRefs = useRef<Record<string, HTMLParagraphElement | null>>({});
83+
84+
useEffect(() => {
85+
const currentTab = tabRefs.current[activeTab];
86+
if (currentTab) {
87+
setUnderlineStyle({
88+
width: currentTab.offsetWidth,
89+
left: currentTab.offsetLeft,
90+
});
91+
}
92+
}, [activeTab]);
93+
94+
const getProgressInfo = (project: Projects) => {
95+
if (project.status === 'Validation') {
96+
return {
97+
current: project.currentVotes,
98+
total: project.targetVotes,
99+
unit: 'Votes',
100+
percentage:
101+
project.currentVotes && project.targetVotes
102+
? (project.currentVotes / project.targetVotes) * 100
103+
: 0,
104+
};
105+
}
106+
107+
if (project.status === 'Funding') {
108+
return {
109+
current:
110+
project.currentAmount && project.targetAmount
111+
? `${project.currentAmount / 1000}k/${project.targetAmount / 1000} USDC`
112+
: 'N/A',
113+
total: null,
114+
unit: 'Raised',
115+
percentage:
116+
project.currentAmount && project.targetAmount
117+
? (project.currentAmount / project.targetAmount) * 100
118+
: 0,
119+
};
120+
}
121+
122+
if (project.status === 'Completed') {
123+
return {
124+
current: project.milestonesCompleted,
125+
total: project.totalMilestones,
126+
unit: 'Milestones Submitted',
127+
percentage:
128+
project.milestonesCompleted && project.totalMilestones
129+
? (project.milestonesCompleted / project.totalMilestones) * 100
130+
: 0,
131+
rejected: project.milestonesRejected,
132+
};
133+
}
134+
135+
return {
136+
current: project.milestonesCompleted,
137+
total: project.totalMilestones,
138+
unit: 'Milestones Submitted',
139+
percentage:
140+
project.milestonesCompleted && project.totalMilestones
141+
? (project.milestonesCompleted / project.totalMilestones) * 100
142+
: 0,
143+
};
144+
};
145+
146+
return (
147+
<section className='relative flex flex-col items-center justify-center text-white'>
148+
<div className='flex flex-col items-center gap-6 text-center'>
149+
<p className='bg-gradient-to-r from-[#3AE6B2] to-[#A7F950] bg-clip-text text-transparent'>
150+
Active Opportunities
151+
</p>
152+
<h2 className='text-5xl max-sm:text-3xl'>Explore What’s Happening</h2>
153+
154+
<div className='relative flex gap-8 overflow-auto border-b border-gray-700 px-4 md:px-0'>
155+
{tabs.map(tab => (
156+
<p
157+
key={tab.value}
158+
ref={el => {
159+
tabRefs.current[tab.value] = el;
160+
}}
161+
onClick={() => setActiveTab(tab.value)}
162+
className={cn(
163+
'shrink-0 cursor-pointer pb-3 transition-colors',
164+
activeTab === tab.value
165+
? 'text-white'
166+
: 'text-gray-400 hover:text-gray-200'
167+
)}
168+
>
169+
{tab.name}
170+
</p>
171+
))}
172+
173+
<span
174+
className='absolute bottom-0 h-[3px] bg-[#A7F950] transition-all duration-300 ease-in-out'
175+
style={underlineStyle}
176+
/>
177+
</div>
178+
</div>
179+
180+
<div className='mt-10 grid w-full max-w-6xl grid-cols-1 gap-6 px-4 md:grid-cols-2 md:px-6 lg:grid-cols-3 xl:px-0'>
181+
{projects.map(project => {
182+
const progressInfo = getProgressInfo(project);
183+
184+
return (
185+
<div
186+
className='max-w-md space-y-5 rounded-xl border border-[#2B2B2B] bg-[#030303] p-4'
187+
key={project.id}
188+
>
189+
{/* Top */}
190+
<div className='flex items-center justify-between'>
191+
<div className='flex items-center gap-2'>
192+
<Image
193+
src='/landing/explore/profile-head.svg'
194+
alt='Profile Head'
195+
width={24}
196+
height={24}
197+
className='aspect-auto rounded-full'
198+
/>
199+
<p className='text-sm text-[#B5B5B5]'>Creator Name</p>
200+
</div>
201+
202+
<div className='flex items-center gap-2'>
203+
<p className='rounded-sm border border-[#645D5D] bg-[#E4DBDB] p-1 text-xs font-semibold text-[#645D5D]'>
204+
Category
205+
</p>
206+
<p
207+
className={cn(
208+
'rounded-sm border border-[#99FF2D] bg-[#A7F950]/8 p-1 text-xs font-semibold text-[#36F94D]',
209+
project.status === 'Validation' &&
210+
'border-[#AD6F07] bg-[#FBE2B7] text-[#AD6F07]',
211+
project.status === 'Funding' &&
212+
'border-[#034592] bg-[#C6DDF7] text-[#034592]',
213+
project.status === 'Completed' &&
214+
'border-[#04802E] bg-[#B5E3C4] text-[#04802E]'
215+
)}
216+
>
217+
{project.status}
218+
</p>
219+
</div>
220+
</div>
221+
222+
{/* Middle */}
223+
<div className='flex gap-4'>
224+
<Image
225+
src={project.projectImg}
226+
alt='Project Image'
227+
width={80}
228+
height={90}
229+
className='aspect-square rounded-lg'
230+
/>
231+
<div className='space-y-2'>
232+
<p className='font-semibold'>Bitmed</p>
233+
<p className='text-sm'>
234+
To build a secure, transparent, and trusted digital health
235+
ecosystem powered by Sonic blockchain for 280M lives in
236+
Indonesia.
237+
</p>
238+
</div>
239+
</div>
240+
241+
{/* Bottom */}
242+
<div className='flex flex-col space-y-2'>
243+
<div className='flex items-center justify-between'>
244+
<p className='flex items-center gap-1 text-sm'>
245+
<span>
246+
{progressInfo.total
247+
? `${progressInfo.current}/${progressInfo.total}`
248+
: progressInfo.current}
249+
</span>
250+
<span className='text-xs text-[#B5B5B5]'>
251+
{progressInfo.unit}
252+
</span>
253+
</p>
254+
{project.daysToDeadline && (
255+
<p
256+
className={cn(
257+
'text-sm',
258+
project.daysToDeadline >= 40 && 'text-[#5FC381]',
259+
project.daysToDeadline < 40 && 'text-[#F5B546]',
260+
project.daysToDeadline < 10 && 'text-[#E26E6A]'
261+
)}
262+
>
263+
{project.daysToDeadline} days to deadline
264+
</p>
265+
)}
266+
{progressInfo.rejected && (
267+
<p className='text-sm text-[#E26E6A]'>
268+
{progressInfo.rejected} milestones rejected
269+
</p>
270+
)}
271+
</div>
272+
<progress
273+
value={progressInfo.percentage || 0}
274+
max={100}
275+
className='h-3 w-full rounded-full [&::-webkit-progress-bar]:rounded-full [&::-webkit-progress-bar]:bg-[#A7F950]/8 [&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:bg-gradient-to-r [&::-webkit-progress-value]:from-[#A7F950]/30 [&::-webkit-progress-value]:to-[#A7F950]'
276+
></progress>
277+
</div>
278+
</div>
279+
);
280+
})}
281+
</div>
282+
283+
{/* Footer */}
284+
<div className='mt-20 flex cursor-pointer items-center gap-1'>
285+
<p className='font-medium underline'>View More Opportunities</p>
286+
<ArrowRight className='h-3 w-3' />
287+
</div>
288+
289+
{/* Glow Effects */}
290+
<Image
291+
src='/landing/explore/explore-glow-top.svg'
292+
alt='Glow Effect'
293+
width={300}
294+
height={200}
295+
className='absolute top-[75px] right-16 -z-[5] max-sm:hidden'
296+
/>
297+
<Image
298+
src='/landing/explore/explore-glow-bottom.svg'
299+
alt='Glow Effect'
300+
width={300}
301+
height={200}
302+
className='absolute bottom-12 left-10 -z-[5] max-sm:hidden'
303+
/>
304+
</section>
305+
);
306+
}

package-lock.json

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

0 commit comments

Comments
 (0)