diff --git a/CHANGELOG.mdx b/CHANGELOG.mdx index 02ac332cf..3b3dbe711 100644 --- a/CHANGELOG.mdx +++ b/CHANGELOG.mdx @@ -17,7 +17,11 @@ Prefix the change with one of these keywords: ## [Unreleased] +### Changed + +- New campaign banner - Updated footer links of FF + ## [0.29.4] ### Added diff --git a/lib/components/CampaignBanner/CampaignBanner.stories.tsx b/lib/components/CampaignBanner/CampaignBanner.stories.tsx index 75e85b756..138a0027d 100644 --- a/lib/components/CampaignBanner/CampaignBanner.stories.tsx +++ b/lib/components/CampaignBanner/CampaignBanner.stories.tsx @@ -1,4 +1,3 @@ -import React from 'react' import type { Meta, StoryObj } from '@storybook/react-vite' import { CampaignBanner } from './CampaignBanner' @@ -17,18 +16,37 @@ export default meta type Story = StoryObj export const Primary: Story = { - args: { - title: 'Fund the Future of Clean Energy Research', - raised: 42500, - goal: 100000, - endDate: '2026-08-27', - categories: [ - { name: 'Environment', link: 'https://example.com/environment' }, - { name: 'Science & Innovation', link: 'https://example.com/science' }, - { name: 'Student Experience' }, - ], - }, + args: {}, render: (args) => { - return + return ( + <> + + +

+ The Music Award for Indigenous, Black and Racialized Students is awarded annually to a student who is + entering or continuing in the undergraduate Music program. +

+
+ + placeholder + + +
+ + ) }, } diff --git a/lib/components/CampaignBanner/CampaignBanner.tsx b/lib/components/CampaignBanner/CampaignBanner.tsx index 3a40424bd..7de7d8a2a 100644 --- a/lib/components/CampaignBanner/CampaignBanner.tsx +++ b/lib/components/CampaignBanner/CampaignBanner.tsx @@ -1,80 +1,20 @@ -import { Badge } from '../Badge/Badge' -import { BadgeGroup } from '../BadgeGroup/BadgeGroup' -import { ButtonGroup } from '../ButtonGroup/ButtonGroup' -import { PageHeader } from '../PageHeader/PageHeader' -import { ProgressBar } from '../ProgressBar/ProgressBar' -import { formatCurrency } from '../../helpers/formatCurrency' - -export interface CampaignCategory { - name: string - link?: string -} - -interface CampaignBannerProps { - title: string - raised: number - goal: number - endDate: string - categories?: CampaignCategory[] -} - -const parseDateLocal = (dateStr: string): Date => { - const [year, month, day] = dateStr.split('-').map(Number) - return new Date(year, month - 1, day) -} - -const getTimeRemainingLabel = (endDate: string): string => { - const daysLeft = Math.ceil((parseDateLocal(endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)) - if (daysLeft > 365) return 'more than a year to go' - if (daysLeft > 30) return `${Math.round(daysLeft / 30)} months to go` - return `${Math.max(daysLeft, 0)} days to go` -} - -export const CampaignBanner = ({ title, raised, goal, endDate, categories }: CampaignBannerProps) => { - const percent = goal > 0 ? Math.min(Math.round((raised / goal) * 100), 100) : 0 - const timeLabel = getTimeRemainingLabel(endDate) - const isActive = parseDateLocal(endDate).getTime() >= Date.now() - const statusText = isActive ? 'Active Campaign' : 'Campaign Completed' - const statusColor = isActive ? 'green' : 'red' +import type { ReactNode } from 'react' +import { CampaignBannerContent } from './CampaignBannerContent' +import { CampaignBannerImage } from './CampaignBannerImage' +import { CampaignBannerStats } from './CampaignBannerStats' +export const CampaignBannerWrapper = ({ children }: { children: ReactNode }) => { return ( -
-
- - - - - - - {categories && categories.length > 0 && ( - - {categories.map((cat) => ( - - ))} - - )} -
-
-
-
- {formatCurrency(raised)} - of {formatCurrency(goal)} -
-
- -
-

- {percent}% funded with {timeLabel} -

- - - Fund this Project - - -
-
-
+
+ {children} +
) } -CampaignBanner.displayName = 'CampaignBanner' +export const CampaignBanner = Object.assign(CampaignBannerWrapper, { + Content: CampaignBannerContent, + Image: CampaignBannerImage, + Stats: CampaignBannerStats, +}) + +CampaignBannerWrapper.displayName = 'CampaignBanner' diff --git a/lib/components/CampaignBanner/CampaignBannerContent.tsx b/lib/components/CampaignBanner/CampaignBannerContent.tsx new file mode 100644 index 000000000..d6a5e9cec --- /dev/null +++ b/lib/components/CampaignBanner/CampaignBannerContent.tsx @@ -0,0 +1,72 @@ +import { Badge } from '../Badge/Badge' +import { ButtonGroup } from '../ButtonGroup/ButtonGroup' +import { PageHeader } from '../PageHeader/PageHeader' +import { Button } from '../Button/Button' +import React from 'react' +import { useLinkContext } from '../LinkProvider/useLinkContext' + +export interface CampaignCategory { + name: string + link: string +} + +export interface CampaignTag { + name: string + link: string +} + +export interface CampaignBannerContentProps { + children?: React.ReactNode + title: string + tags?: CampaignTag[] + categories?: CampaignCategory[] +} + +export const CampaignBannerContent = ({ title, tags, categories, children }: CampaignBannerContentProps) => { + const LinkComponent = useLinkContext() + + const tagNames = + tags && tags.length > 0 + ? tags.map((tag, index) => ( + <> + + {tag.name} + + {index < tags.length - 1 ? ', ' : null} + + )) + : '' + + return ( +
+ {/* Page Header */} + + {children} + + + {/* Categories */} + {categories && categories.length > 0 && ( +
+ {categories.map((cat) => ( + + + + ))} +
+ )} + + +
+ ) +} + +CampaignBannerContent.displayName = 'CampaignBanner.Content' diff --git a/lib/components/CampaignBanner/CampaignBannerImage.tsx b/lib/components/CampaignBanner/CampaignBannerImage.tsx new file mode 100644 index 000000000..cc6436a95 --- /dev/null +++ b/lib/components/CampaignBanner/CampaignBannerImage.tsx @@ -0,0 +1,9 @@ +export interface CampaignBannerImageProps { + children: React.ReactNode +} + +export const CampaignBannerImage = ({ children }: CampaignBannerImageProps) => { + return
{children}
+} + +CampaignBannerImage.displayName = 'CampaignBanner.Image' diff --git a/lib/components/CampaignBanner/CampaignBannerStats.tsx b/lib/components/CampaignBanner/CampaignBannerStats.tsx new file mode 100644 index 000000000..37cebe35e --- /dev/null +++ b/lib/components/CampaignBanner/CampaignBannerStats.tsx @@ -0,0 +1,65 @@ +import { formatCurrency } from '../../helpers/formatCurrency' +import { Column } from '../../layouts/Column/Column' +import { Card } from '../Card/Card' + +export interface CampaignBannerStatsProps { + raised: number + goal: number + endDate: string +} + +const parseDateLocal = (dateStr: string): Date => { + const [year, month, day] = dateStr.split('-').map(Number) + return new Date(year, month - 1, day) +} + +const getTimeRemainingLabel = (endDate: string): string => { + const daysLeft = Math.ceil((parseDateLocal(endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)) + if (daysLeft > 365) return 'more than a year to go' + if (daysLeft > 30) return `${Math.round(daysLeft / 30)} months to go` + return `${Math.max(daysLeft, 0)} days left` +} + +export const CampaignBannerStats = ({ raised, goal, endDate }: CampaignBannerStatsProps) => { + const percent = goal > 0 ? Math.min(Math.round((raised / goal) * 100), 100) : 0 + + const StatData = [ + { + id: 'database-regular', + stat: formatCurrency(raised), + desc: 'Amount Raised', + }, + { + id: 'bullseye-arrow', + stat: formatCurrency(goal), + desc: 'Goal', + }, + { + id: 'file-chart-pie', + stat: `${percent}%`, + desc: 'of Goal Reached', + reverse: true, + }, + { + id: 'calendar-days', + stat: `${getTimeRemainingLabel(endDate)}`, + desc: '', + }, + ] + + return ( +
+ + {StatData.slice(0, 4).map(({ id, stat, desc, reverse }) => ( + <> + + + + + ))} + +
+ ) +} + +CampaignBannerStats.displayName = 'CampaignBanner.Stats' diff --git a/lib/components/CampaignBanner/styles.css b/lib/components/CampaignBanner/styles.css index e280bd3d8..39dc751fc 100644 --- a/lib/components/CampaignBanner/styles.css +++ b/lib/components/CampaignBanner/styles.css @@ -1,11 +1,28 @@ -.cu-campaign-banner p + .cu-badgegroup { - @apply pt-5; +.cu-section > .cu-campaign-banner { + margin-top: -40px; + @apply -mt-6 lg:-mt-10; } -.cu-campaign-banner .cu-pageheader { - @apply mt-6 lg:mt-8; +.cu-campaign-banner__image { + position: relative; + background-color: #191919; } -.cu-campaign-banner .cu-badgegroup:not(:first-child) { - @apply mt-4 lg:mt-6; +.cu-campaign-banner__image:after { + content: ''; + display: block; + position: absolute; + top: 0; + right: 0; + width: calc(100% - 1024px / 2); + height: 100%; + background-image: linear-gradient(270deg, #191919, transparent); +} + +.cu-campaign-banner__image img { + object-fit: cover; + width: 100%; + height: 100%; + margin: 0; + border-radius: 0; } diff --git a/lib/components/PageHeader/PageHeader.tsx b/lib/components/PageHeader/PageHeader.tsx index fb56c9e6c..e9034f16e 100644 --- a/lib/components/PageHeader/PageHeader.tsx +++ b/lib/components/PageHeader/PageHeader.tsx @@ -15,7 +15,7 @@ const SIZE_CONFIG = { export interface PageHeaderProps { children?: React.ReactNode as?: 'h1' | 'h2' | 'h3' - preHeader?: string + preHeader?: string | React.ReactNode header: string content?: string metaData?: React.ReactNode diff --git a/lib/examples/projects/futurefunder/SingleProject.stories.tsx b/lib/examples/projects/futurefunder/SingleProject.stories.tsx index ff15c7a46..1b38e98a5 100644 --- a/lib/examples/projects/futurefunder/SingleProject.stories.tsx +++ b/lib/examples/projects/futurefunder/SingleProject.stories.tsx @@ -7,6 +7,7 @@ import { PageHeader } from '../../../components/PageHeader/PageHeader' import { Figure } from '../../../components/Figure/Figure' import { CampaignBanner } from '../../../components/CampaignBanner/CampaignBanner' import { NavButtonsData, NavFutureFunder } from '../../../data/NavData' +import { Section } from '../../../layouts/Section/Section' const meta: Meta = { title: 'Projects/FutureFunder', @@ -48,51 +49,56 @@ export const SingleProject: Story = {
- - - {/*
+ + +

+ The Music Award for Indigenous, Black and Racialized Students is awarded annually to a student who is + entering or continuing in the undergraduate Music program. +

+
+ + placeholder + + +
+
+ + +
Sample for figure stories file -
*/} - - - -
- Sample for figure stories file -
+
- - + + - - + + - - + + - -

The primary donation form would be placed here.

+ +

The primary donation form would be placed here.

+