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
30 changes: 5 additions & 25 deletions lib/components/Card/CardFundingMeta.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Icon } from '../Icon'
import { useEffect, useState } from 'react'
import { ProgressBar } from '../ProgressBar/ProgressBar'
import { formatCurrency } from '../../helpers/formatCurrency'

interface CardFundingMetaProps {
raised: number
Expand All @@ -15,25 +16,7 @@ const styles = {
}

export function CardFundingMeta({ raised, goal, daysLeft, showPercentage = true }: CardFundingMetaProps) {
const [animatedWidth, setAnimatedWidth] = useState(0)
const percentage = (raised / goal) * 100

// Animate progress bar on mount
useEffect(() => {
const timer = setTimeout(() => {
setAnimatedWidth(percentage)
}, 100)
return () => clearTimeout(timer)
}, [percentage])

const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount)
}
const percentage = goal > 0 ? (raised / goal) * 100 : 0

const getTimeText = () => {
if (daysLeft === 0) return 'Last day'
Expand All @@ -53,11 +36,8 @@ export function CardFundingMeta({ raised, goal, daysLeft, showPercentage = true
</div>

{/* Progress Bar */}
<div className="h-3 bg-slate-200 rounded-full overflow-hidden mt-2 mb-3.5">
<div
className="h-full bg-gradient-to-r from-cu-red-400 to-cu-red-600 rounded-full transition-all duration-1000 ease-out"
style={{ width: `${animatedWidth}%` }}
/>
<div className="mt-2 mb-3.5">
<ProgressBar value={raised} max={goal} />
</div>

{/* Bottom Info */}
Expand Down
16 changes: 16 additions & 0 deletions lib/components/FundingDetails/FundingDetails copy 2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface FundingDetailsProps {
image?: string
}

export const FundingDetails = ({ image }: FundingDetailsProps) => {
console.log('FundingDetails image prop:', image) // Debug log to check the image prop value

return (
<div className="bg-cu-black-50">
<div className="">Left</div>
<div className="">Right</div>
</div>
)
}

FundingDetails.displayName = 'FundingDetails'
104 changes: 104 additions & 0 deletions lib/components/FundingDetails/FundingDetails copy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useState } from 'react'

const ERROR_IMG_SRC =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='

const DEFAULT_IMAGE =
'https://fastly.picsum.photos/id/237/1200/800.jpg?hmac=Zig5Q0Oa_5oSGNOhgbpE-lgHzdREZIxTf94rVP1-uCg'

export interface FundingDetailsProps {
image?: string
}

export const FundingDetails = ({ image }: FundingDetailsProps) => {
const [didError, setDidError] = useState(false)

// Sample data - replace with actual campaign data
const raised = 45250
const goal = 75000
const percentComplete = (raised / goal) * 100
const supporters = 342
const daysLeft = 28

return (
<div className="w-screen ml-offset-center mt-1.5 2xl:mt-10">
{/* Hero Container */}
<div className="max-w-screen-2xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-5 min-h-[500px] lg:min-h-[600px]">
{/* Left Side - Campaign Content (Desktop) / Image First (Mobile) */}
<div className="bg-white flex items-center lg:col-span-2 order-2 lg:order-1">
<div className="w-full px-6 py-12 md:px-12 lg:px-12 lg:py-16">
{/* Header */}
<div className="mb-8">
<span className="inline-block bg-red-100 text-red-800 text-xs font-medium px-3 py-1.5 mb-4 uppercase tracking-wide">
Active Campaign
</span>
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-3">Annual Musical Production 2026</h1>
<p className="text-base text-gray-600">Help bring the magic of live theater to our campus community</p>
</div>

{/* Campaign Progress */}
<div className="mb-8">
<div className="flex items-baseline gap-3 mb-4">
<span className="text-4xl font-bold text-gray-900">${raised.toLocaleString()}</span>
<span className="text-base text-gray-600">of ${goal.toLocaleString()}</span>
</div>

{/* Progress Bar */}
<div className="w-full h-3 bg-gray-200 rounded-sm overflow-hidden mb-2">
<div
className="h-full bg-red-700 transition-all duration-500"
style={{ width: `${percentComplete}%` }}
/>
</div>
<div className="text-sm font-medium text-gray-700">{Math.round(percentComplete)}% funded</div>
</div>

{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-2 gap-6 mb-10 pb-8 border-b border-gray-200">
<div className="text-center md:text-left">
<div className="text-2xl font-bold text-gray-900">{Math.round(percentComplete)}%</div>
<div className="text-sm text-gray-600 mt-1">Complete</div>
</div>
<div className="text-center md:text-left">
<div className="text-2xl font-bold text-gray-900">{supporters}</div>
<div className="text-sm text-gray-600 mt-1">Supporters</div>
</div>
<div className="text-center md:text-left">
<div className="text-2xl font-bold text-gray-900">{daysLeft}</div>
<div className="text-sm text-gray-600 mt-1">Days Left</div>
</div>
<div className="text-center md:text-left">
<div className="text-2xl font-bold text-gray-900">${Math.round(raised / supporters)}</div>
<div className="text-sm text-gray-600 mt-1">Average</div>
</div>
</div>

{/* Call to Action */}
<div className="flex flex-col sm:flex-row gap-4">
<button className="flex-1 bg-red-700 hover:bg-red-800 text-white font-bold py-4 px-8 rounded transition-colors duration-200">
Fund this Project
</button>
<button className="flex-1 sm:flex-initial bg-white hover:bg-gray-50 text-gray-900 font-bold py-4 px-8 rounded border-2 border-gray-300 hover:border-gray-400 transition-colors duration-200">
Learn More
</button>
</div>
</div>
</div>

{/* Right Side - Image */}
<div className="relative lg:h-auto lg:col-span-3 order-1 lg:order-2">
<img
src={didError ? ERROR_IMG_SRC : image || DEFAULT_IMAGE}
alt="Students performing in musical production"
className="w-full h-full object-cover !m-0 !rounded-none 2xl:!rounded-lg"
onError={() => setDidError(true)}
/>
</div>
</div>
</div>
</div>
)
}

FundingDetails.displayName = 'FundingDetails'
24 changes: 24 additions & 0 deletions lib/components/FundingDetails/FundingDetails.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'
import type { Meta, StoryObj } from '@storybook/react-vite'
import { FundingDetails } from './FundingDetails'

const meta: Meta<typeof FundingDetails> = {
title: 'Components/Funding Details',
component: FundingDetails,
tags: ['autodocs'],
parameters: {
controls: {
sort: 'requiredFirst',
},
},
}

export default meta
type Story = StoryObj<typeof FundingDetails>

export const Primary: Story = {
args: {},
render: (args) => {
return <FundingDetails {...args} />
},
}
61 changes: 61 additions & 0 deletions lib/components/FundingDetails/FundingDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Badge } from '../Badge/Badge'
import { BadgeGroup } from '../BadgeGroup/BadgeGroup'
import { Button } from '../Button/Button'
import { ButtonGroup } from '../ButtonGroup/ButtonGroup'
import { PageHeader } from '../PageHeader/PageHeader'
import { ProgressBar } from '../ProgressBar/ProgressBar'
import { formatCurrency } from '../../helpers/formatCurrency'

interface FundingDetailsProps {
status: string
title: string
raised: number
goal: number
imageUrl: string
imageAlt: string
}

export const FundingDetails = ({ status, title, raised, goal, imageUrl, imageAlt }: FundingDetailsProps) => {
const percent = goal > 0 ? Math.min(Math.round((raised / goal) * 100), 100) : 0

return (
<>
<div className="flex flex-col lg:flex-row items-stretch gap-8 lg:gap-16 lg:border-b lg:border-cu-black-300/50 lg:rounded-sm lg:pb-8">
<div className="lg:w-1/2 lg:py-10">
<BadgeGroup bottom={0} gap="2" left={0} right={0} top={0}>
<Badge color="green" text={status} />
</BadgeGroup>

<PageHeader as="h1" header={title} size="lg" noUnderline />

<div className="mb-2">
<span className="text-4xl font-bold">{formatCurrency(raised)}</span>
<span className="text-base text-cu-black-400 ml-2">of {formatCurrency(goal)}</span>
</div>

<div className="mb-1.5">
<ProgressBar value={raised} max={goal} />
</div>

<p className="text-sm text-cu-black-600 mb-8">{percent}% funded</p>

<ButtonGroup align="start" gap="5">
<Button onClick={() => {}} title="Fund this Project" />
</ButtonGroup>
</div>

{/* Image */}
<div className="lg:w-1/2">
<img
src={imageUrl}
alt={imageAlt}
className="w-full h-full object-cover rounded-xl !m-0"
// style={{ minHeight: '380px' }}
/>
</div>
</div>
</>
)
}

FundingDetails.displayName = 'FundingDetails'
30 changes: 30 additions & 0 deletions lib/components/FundingDetails/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* TODO GLOBAL: negative top based on primary spacing */
.cu-main > .cu-section > .cu-fullbanner:first-of-type {
@apply -mt-6 lg:-mt-10;
}

.cu-main > .cu-section > .cu-fullbanner:first-of-type video {
@apply mt-0;
}

/* TODO GLOBAL: negative bottom based on primary spacing */
.cu-main > .cu-section--primary > .cu-fullbanner:last-child {
@apply -mb-6 lg:-mb-10;
}

.cu-fullbanner .cu-pageheader {
@apply mb-0;
}

.cu-fullbanner img {
@apply rounded-none m-0;
}

.cu-fullbanner h1,
.cu-fullbanner h2,
.cu-fullbanner h3,
.cu-fullbanner h4,
.cu-fullbanner h5,
.cu-fullbanner h6 {
@apply text-white;
}
38 changes: 38 additions & 0 deletions lib/components/ProgressBar/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useEffect, useState } from 'react'

interface ProgressBarProps {
value: number
max?: number
}

export const ProgressBar = ({ value, max = 100 }: ProgressBarProps) => {
const [animatedWidth, setAnimatedWidth] = useState(0)

// Clamp percentage between 0 and 100
const percentage = max > 0 ? Math.min(Math.round((value / max) * 100), 100) : 0

// Animate progress bar on mount
useEffect(() => {
const timer = setTimeout(() => {
setAnimatedWidth(percentage)
}, 100)
return () => clearTimeout(timer)
}, [percentage])

return (
<div
className="h-3 bg-slate-200 rounded-full overflow-hidden"
role="progressbar"
aria-valuenow={percentage}
aria-valuemin={0}
aria-valuemax={100}
>
<div
className="h-full bg-gradient-to-r from-cu-red-400 to-cu-red-600 rounded-full transition-all duration-1000 ease-out"
style={{ width: `${animatedWidth}%` }}
/>
</div>
)
}

ProgressBar.displayName = 'ProgressBar'
68 changes: 68 additions & 0 deletions lib/examples/projects/futurefunder/ProjectLayout.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react'
import type { Meta, StoryObj } from '@storybook/react-vite'
import { Main } from '../../../layouts/Main/Main'
import { Section } from '../../../layouts/Section/Section'
import { FooterStandard } from '../../../components/Footer/FooterStandard/FooterStandard'
import { Nav } from '../../../components/Nav/Nav'
import { FundingDetails } from '../../../components/FundingDetails/FundingDetails'
import { NavButtonsData, NavFutureFunder } from '../../../data/NavData'

const meta: Meta = {
title: 'Projects/FutureFunder',
parameters: {
layout: 'fullscreen',
},
}

export default meta
type Story = StoryObj

const SinglePara = () => {
return (
<>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sit amet tortor pellentesque, posuere tellus
vitae, sagittis justo. Vivamus imperdiet turpis nec elit ultricies. Suspendisse condimentum magna vel orci
vulputate, eget vulputate neque porttitor. Suspendisse euismod, urna et gravida volutpat, tortor risus vehicula
nisl, in vulputate lectus dolor viverra est. Etiam quis interdum nisi, et malesuada lectus. Aliquam luctus,
velit eget suscipit tincidunt, sem ex tempus turpis, quis pulvinar metus sapien in urna.
</p>
</>
)
}

export const ProjectLayout: Story = {
render: () => (
<>
<Nav>
<Nav.Top>
<Nav.Logo title="FutureFunder" link="https://graduate.carleton.ca" />
<Nav.Buttons menu={NavButtonsData} isSearch onClickSearch={() => {}} />
</Nav.Top>
<Nav.Bottom>
<Nav.Menu menu={NavFutureFunder} />
<Nav.Buttons menu={NavButtonsData} isSearch onClickSearch={() => {}} />
</Nav.Bottom>
</Nav>

<Main>
<Section as="div" maxWidth="max">
<FundingDetails
status="ACTIVE CAMPAIGN"
title="Annual Musical Production 2026"
raised={45250}
goal={75000}
imageUrl="https://fastly.picsum.photos/id/237/1200/800.jpg?hmac=Zig5Q0Oa_5oSGNOhgbpE-lgHzdREZIxTf94rVP1-uCg"
imageAlt="Black labrador puppy looking up"
/>
</Section>

<SinglePara />
<SinglePara />
<SinglePara />
</Main>

<FooterStandard />
</>
),
}
Loading