Skip to content

Commit ab186c3

Browse files
feat(components): Implement dynamic ProjectCard with conditional (#265)
rendering and styles.
1 parent 1340204 commit ab186c3

5 files changed

Lines changed: 221 additions & 5 deletions

File tree

app/(landing)/projects/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ export const metadata: Metadata = generatePageMetadata('projects');
77
const ProjectsPage = () => {
88
return (
99
<div className='text-white text-4xl font-bold text-center mt-10'>
10-
Projects Page
10+
<h1 className='mb-10'>Projects</h1>
11+
<div className='flex flex-wrap gap-8 justify-center'>
12+
{/* Project cards will be rendered here */}
13+
</div>
1114
</div>
1215
);
1316
};

app/globals.css

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@import 'tailwindcss';
22
@import 'tw-animate-css';
3-
3+
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
44
@custom-variant dark (&:is(.dark *));
55

66
/* Custom scrollbar styles for comment modal */
@@ -70,6 +70,15 @@
7070
--radius-md: calc(var(--radius) - 2px);
7171
--radius-lg: var(--radius);
7272
--radius-xl: calc(var(--radius) + 4px);
73+
--color-office-brown: var(--office-brown);
74+
--color-office-brown-darker: var(--office-brown-darker);
75+
--color-warning-orange: var(--warning-orange);
76+
--color-warning-orange-darker: var(--warning-orange-darker);
77+
--color-blue-ish: var(--blue-ish);
78+
--color-blue-ish-darker: var(--blue-ish-darker);
79+
--color-success-green: var(--success-green);
80+
--color-success-green-darker: var(--success-green-darker);
81+
--color-error-status: var(--error-status);
7382

7483
/* Custom breakpoints for 4K screens */
7584
--breakpoint-4xl: 2560px;
@@ -113,6 +122,15 @@
113122
--sidebar-accent-foreground: oklch(0.205 0 0);
114123
--sidebar-border: oklch(0.922 0 0);
115124
--sidebar-ring: oklch(0.708 0 0);
125+
--office-brown: #e4dbdb;
126+
--office-brown-darker: #645d5d;
127+
--warning-orange: #fbe2b7;
128+
--warning-orange-darker: #ad6f07;
129+
--blue-ish: #c6ddf7;
130+
--blue-ish-darker: #034592;
131+
--success-green: #b5e3c4;
132+
--success-green-darker: #04802e;
133+
--error-status: #034592;
116134
}
117135

118136
.dark {
@@ -147,6 +165,15 @@
147165
--sidebar-accent-foreground: oklch(0.985 0 0);
148166
--sidebar-border: oklch(1 0 0 / 10%);
149167
--sidebar-ring: oklch(0.556 0 0);
168+
--office-brown: #645d5d;
169+
--office-brown-darker: #e4dbdb;
170+
--warning-orange: #ad6f07;
171+
--warning-orange-darker: #fbe2b7;
172+
--blue-ish: #034592;
173+
--blue-ish-darker: #c6ddf7;
174+
--success-green: #04802e;
175+
--success-green-darker: #b5e3c4;
176+
--error-status: #034592;
150177
}
151178

152179
@layer base {
@@ -196,7 +223,9 @@ input[type='number'] {
196223
-webkit-background-clip: text;
197224
-webkit-text-fill-color: transparent;
198225
}
199-
226+
.font-inter {
227+
font-family: 'Inter', sans-serif;
228+
}
200229
/* Dropdown menu item animations */
201230
@keyframes dropdownItemSlideIn {
202231
from {
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { Progress } from '@/components/ui/progress';
2+
import { formatNumber } from '@/lib/utils';
3+
4+
type ProjectCardProps = {
5+
creatorName: string;
6+
creatorLogo: string;
7+
projectImage: string;
8+
projectTitle: string;
9+
projectDescription: string;
10+
status: 'Validation' | 'Funding' | 'Funded' | 'Completed';
11+
deadlineInDays: number;
12+
milestoneRejected?: boolean;
13+
votes?: {
14+
current: number;
15+
goal: number;
16+
};
17+
funding?: {
18+
current: number;
19+
goal: number;
20+
currency: string;
21+
};
22+
milestones?: {
23+
current: number;
24+
goal: number;
25+
};
26+
};
27+
function ProjectCard({
28+
creatorName,
29+
creatorLogo,
30+
projectImage,
31+
projectTitle,
32+
projectDescription,
33+
status,
34+
deadlineInDays,
35+
milestoneRejected,
36+
votes,
37+
funding,
38+
milestones,
39+
}: ProjectCardProps) {
40+
const getStatusStyles = () => {
41+
switch (status) {
42+
case 'Funding':
43+
return 'bg-blue-ish border-blue-ish-darker text-blue-ish-darker';
44+
case 'Funded':
45+
return 'bg-transparent border-primary text-primary';
46+
case 'Completed':
47+
return 'bg-success-green border-success-green-darker text-success-green-darker';
48+
case 'Validation':
49+
return 'bg-warning-orange border-warning-orange-darker text-warning-orange-darker';
50+
default:
51+
return '';
52+
}
53+
};
54+
55+
const getDeadlineInfo = () => {
56+
if (status === 'Completed' && milestoneRejected) {
57+
return {
58+
text: '1 Milestone Rejected',
59+
className: 'text-red-500',
60+
};
61+
}
62+
63+
if (deadlineInDays <= 3) {
64+
return {
65+
text: `${deadlineInDays} days to deadline`,
66+
className: 'text-error-status',
67+
};
68+
}
69+
70+
if (deadlineInDays <= 15) {
71+
return {
72+
text: `${deadlineInDays} days to deadline`,
73+
className: 'text-warning-orange-darker',
74+
};
75+
}
76+
77+
return {
78+
text: `${deadlineInDays} days to deadline`,
79+
className: 'text-success-green-darker',
80+
};
81+
};
82+
83+
const deadlineInfo = getDeadlineInfo();
84+
85+
return (
86+
<main className='w-[397px] flex flex-col gap-4 p-5 rounded-[8px] bg-[#030303] border border-gray-900 font-inter hover:bg-primary/5 hover:border-primary/25 cursor-pointer'>
87+
<header className='flex items-center justify-between'>
88+
<main className='flex items-center gap-2'>
89+
<div
90+
style={{ backgroundImage: `url(${creatorLogo})` }}
91+
className='size-6 rounded-full bg-white bg-cover bg-center'
92+
></div>
93+
<h4 className='font-normal text-sm text-gray-500'>{creatorName}</h4>
94+
</main>
95+
<main className='flex items-center gap-3'>
96+
<div className='w-[63px] px-1 py-0.5 rounded-[4px] bg-office-brown border border-office-brown-darker text-office-brown-darker font-semibold text-xs flex items-center justify-center '>
97+
Category
98+
</div>
99+
<div
100+
className={`px-1 py-0.5 rounded-[4px] ${getStatusStyles()} border font-semibold text-xs flex items-center justify-center `}
101+
>
102+
{status}
103+
</div>
104+
</main>
105+
</header>
106+
<article className='flex items-center gap-5'>
107+
<main
108+
style={{ backgroundImage: `url(${projectImage})` }}
109+
className='bg-white w-[79.41px] h-[90px] rounded-[8px] bg-cover bg-center'
110+
></main>
111+
<main className='flex flex-col gap-2 w-[257px]'>
112+
<h2 className='font-semibold text-base text-white text-left'>
113+
{projectTitle}
114+
</h2>
115+
<div className='relative h-[60px] group'>
116+
<p className='font-normal text-sm text-white text-left line-clamp-3 cursor-pointer group-hover:absolute group-hover:z-10 group-hover:bg-[#030303] group-hover:rounded-md group-hover:line-clamp-none group-hover:w-full'>
117+
{projectDescription}
118+
</p>
119+
</div>
120+
</main>
121+
</article>
122+
<footer className='flex flex-col gap-2'>
123+
<main className='flex items-center justify-between'>
124+
{status === 'Validation' && votes && (
125+
<h3 className='font-medium text-sm text-[#f5f5f5]'>
126+
{formatNumber(votes.current)}/{formatNumber(votes.goal)}{' '}
127+
<span className='text-gray-500'>Votes</span>
128+
</h3>
129+
)}
130+
{status === 'Funding' && funding && (
131+
<h3 className='font-medium text-sm text-[#f5f5f5]'>
132+
{formatNumber(funding.current)}/{formatNumber(funding.goal)}{' '}
133+
<span className='text-gray-500'>{funding.currency} raised</span>
134+
</h3>
135+
)}
136+
{(status === 'Funded' || status === 'Completed') && milestones && (
137+
<h3 className='font-medium text-sm text-[#f5f5f5]'>
138+
{milestones.current}/{milestones.goal}{' '}
139+
<span className='text-gray-500'>Milestones Submitted</span>
140+
</h3>
141+
)}
142+
143+
<h3 className={`font-medium text-sm ${deadlineInfo.className}`}>
144+
{deadlineInfo.text}
145+
</h3>
146+
</main>
147+
<div>
148+
<Progress
149+
value={
150+
status === 'Validation'
151+
? votes
152+
? (votes.current / votes.goal) * 100
153+
: 0
154+
: status === 'Funding'
155+
? funding
156+
? (funding.current / funding.goal) * 100
157+
: 0
158+
: milestones
159+
? (milestones.current / milestones.goal) * 100
160+
: 0
161+
}
162+
className='w-[357px] h-2 rounded-full'
163+
/>
164+
</div>
165+
</footer>
166+
</main>
167+
);
168+
}
169+
170+
export default ProjectCard;

components/ui/progress.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ function Progress({
2121
>
2222
<ProgressPrimitive.Indicator
2323
data-slot='progress-indicator'
24-
className='bg-primary h-full w-full flex-1 transition-all'
25-
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
24+
className='h-full w-full flex-1 transition-all rounded-full'
25+
style={{
26+
transform: `translateX(-${100 - (value || 0)}%)`,
27+
backgroundImage:
28+
'linear-gradient(to right, rgba(167, 249, 80, 0.5) 0%, rgba(167, 249, 80, 0.6) 20%, rgba(167, 249, 80, 0.7) 40%, rgba(167, 249, 80, 0.8) 60%, rgba(167, 249, 80, 0.9) 80%, rgba(167, 249, 80, 1) 100%)',
29+
}}
2630
/>
2731
</ProgressPrimitive.Root>
2832
);

lib/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,13 @@ export const maskEmail = (email: string) => {
9595
export const generateCampaignLink = async (projectId: string) => {
9696
return `${process.env.NEXT_PUBLIC_APP_URL}/project/${projectId}`;
9797
};
98+
99+
export const formatNumber = (num: number): string => {
100+
if (num >= 1000000) {
101+
return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
102+
}
103+
if (num >= 1000) {
104+
return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
105+
}
106+
return num.toString();
107+
};

0 commit comments

Comments
 (0)