From e6c6f3bd2b9b01dd3d0751c464d4c68cd371d152 Mon Sep 17 00:00:00 2001 From: marvelousmicheal Date: Sat, 20 Sep 2025 14:11:23 +0100 Subject: [PATCH] feat(components): Implement dynamic ProjectCard with conditional rendering and styles. --- app/(landing)/projects/page.tsx | 5 +- app/globals.css | 33 +++- .../landing-page/project/ProjectCard.tsx | 170 ++++++++++++++++++ components/ui/progress.tsx | 8 +- lib/utils.ts | 10 ++ 5 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 components/landing-page/project/ProjectCard.tsx diff --git a/app/(landing)/projects/page.tsx b/app/(landing)/projects/page.tsx index 7ee3a1f3a..872641336 100644 --- a/app/(landing)/projects/page.tsx +++ b/app/(landing)/projects/page.tsx @@ -7,7 +7,10 @@ export const metadata: Metadata = generatePageMetadata('projects'); const ProjectsPage = () => { return (
- Projects Page +

Projects

+
+ {/* Project cards will be rendered here */} +
); }; diff --git a/app/globals.css b/app/globals.css index 76cf79fb0..f8284d924 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,6 +1,6 @@ @import 'tailwindcss'; @import 'tw-animate-css'; - +@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); @custom-variant dark (&:is(.dark *)); /* Custom scrollbar styles for comment modal */ @@ -70,6 +70,15 @@ --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); + --color-office-brown: var(--office-brown); + --color-office-brown-darker: var(--office-brown-darker); + --color-warning-orange: var(--warning-orange); + --color-warning-orange-darker: var(--warning-orange-darker); + --color-blue-ish: var(--blue-ish); + --color-blue-ish-darker: var(--blue-ish-darker); + --color-success-green: var(--success-green); + --color-success-green-darker: var(--success-green-darker); + --color-error-status: var(--error-status); /* Custom breakpoints for 4K screens */ --breakpoint-4xl: 2560px; @@ -113,6 +122,15 @@ --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); + --office-brown: #e4dbdb; + --office-brown-darker: #645d5d; + --warning-orange: #fbe2b7; + --warning-orange-darker: #ad6f07; + --blue-ish: #c6ddf7; + --blue-ish-darker: #034592; + --success-green: #b5e3c4; + --success-green-darker: #04802e; + --error-status: #034592; } .dark { @@ -147,6 +165,15 @@ --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); --sidebar-ring: oklch(0.556 0 0); + --office-brown: #645d5d; + --office-brown-darker: #e4dbdb; + --warning-orange: #ad6f07; + --warning-orange-darker: #fbe2b7; + --blue-ish: #034592; + --blue-ish-darker: #c6ddf7; + --success-green: #04802e; + --success-green-darker: #b5e3c4; + --error-status: #034592; } @layer base { @@ -196,7 +223,9 @@ input[type='number'] { -webkit-background-clip: text; -webkit-text-fill-color: transparent; } - +.font-inter { + font-family: 'Inter', sans-serif; +} /* Dropdown menu item animations */ @keyframes dropdownItemSlideIn { from { diff --git a/components/landing-page/project/ProjectCard.tsx b/components/landing-page/project/ProjectCard.tsx new file mode 100644 index 000000000..fae758e60 --- /dev/null +++ b/components/landing-page/project/ProjectCard.tsx @@ -0,0 +1,170 @@ +import { Progress } from '@/components/ui/progress'; +import { formatNumber } from '@/lib/utils'; + +type ProjectCardProps = { + creatorName: string; + creatorLogo: string; + projectImage: string; + projectTitle: string; + projectDescription: string; + status: 'Validation' | 'Funding' | 'Funded' | 'Completed'; + deadlineInDays: number; + milestoneRejected?: boolean; + votes?: { + current: number; + goal: number; + }; + funding?: { + current: number; + goal: number; + currency: string; + }; + milestones?: { + current: number; + goal: number; + }; +}; +function ProjectCard({ + creatorName, + creatorLogo, + projectImage, + projectTitle, + projectDescription, + status, + deadlineInDays, + milestoneRejected, + votes, + funding, + milestones, +}: ProjectCardProps) { + const getStatusStyles = () => { + switch (status) { + case 'Funding': + return 'bg-blue-ish border-blue-ish-darker text-blue-ish-darker'; + case 'Funded': + return 'bg-transparent border-primary text-primary'; + case 'Completed': + return 'bg-success-green border-success-green-darker text-success-green-darker'; + case 'Validation': + return 'bg-warning-orange border-warning-orange-darker text-warning-orange-darker'; + default: + return ''; + } + }; + + const getDeadlineInfo = () => { + if (status === 'Completed' && milestoneRejected) { + return { + text: '1 Milestone Rejected', + className: 'text-red-500', + }; + } + + if (deadlineInDays <= 3) { + return { + text: `${deadlineInDays} days to deadline`, + className: 'text-error-status', + }; + } + + if (deadlineInDays <= 15) { + return { + text: `${deadlineInDays} days to deadline`, + className: 'text-warning-orange-darker', + }; + } + + return { + text: `${deadlineInDays} days to deadline`, + className: 'text-success-green-darker', + }; + }; + + const deadlineInfo = getDeadlineInfo(); + + return ( +
+
+
+
+

{creatorName}

+
+
+
+ Category +
+
+ {status} +
+
+
+
+
+
+

+ {projectTitle} +

+
+

+ {projectDescription} +

+
+
+
+
+
+ {status === 'Validation' && votes && ( +

+ {formatNumber(votes.current)}/{formatNumber(votes.goal)}{' '} + Votes +

+ )} + {status === 'Funding' && funding && ( +

+ {formatNumber(funding.current)}/{formatNumber(funding.goal)}{' '} + {funding.currency} raised +

+ )} + {(status === 'Funded' || status === 'Completed') && milestones && ( +

+ {milestones.current}/{milestones.goal}{' '} + Milestones Submitted +

+ )} + +

+ {deadlineInfo.text} +

+
+
+ +
+
+
+ ); +} + +export default ProjectCard; diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx index fb12f8137..725f49c87 100644 --- a/components/ui/progress.tsx +++ b/components/ui/progress.tsx @@ -21,8 +21,12 @@ function Progress({ > ); diff --git a/lib/utils.ts b/lib/utils.ts index 8e7abc25e..eb0c8a5a1 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -95,3 +95,13 @@ export const maskEmail = (email: string) => { export const generateCampaignLink = async (projectId: string) => { return `${process.env.NEXT_PUBLIC_APP_URL}/project/${projectId}`; }; + +export const formatNumber = (num: number): string => { + if (num >= 1000000) { + return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M'; + } + if (num >= 1000) { + return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; + } + return num.toString(); +};