Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GIVCAMP-88 | data card #245

Merged
merged 10 commits into from
Mar 12, 2024
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# [Giving Campaign (Stanford On Purpose)](https://github.com/SU-SWS/ood-giving-campaign)
# [Stanford Momentum](https://github.com/SU-SWS/ood-giving-campaign)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating some readme

[![Netlify Status](https://api.netlify.com/api/v1/badges/738e5599-7329-41a1-8429-82f8540636d9/deploy-status?branch=dev)](https://app.netlify.com/sites/giving-campaign/deploys)

Description
---

Netlify hosted, Next.js built, Storyblok headless CMS site for the Stanford On Purpose website.
Netlify hosted, Next.js built, Storyblok headless CMS site for the Stanford Momentum website.

Environment variable set up and installation
---
Expand Down
16 changes: 16 additions & 0 deletions components/DataCard/DataCard.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { cnb } from 'cnbuilder';

export const animateWrapper = 'h-full';
// Use border-black-50/50 which works well on both light and dark backgrounds
export const root = 'relative overflow-hidden size-full break-words border-l-2 border-black-50/50';

export const flex = 'h-full';
export const content = (
hasBarColor?: boolean,
) => cnb('rs-pl-2', {
'border-l-[1.4rem] md:border-l-[2rem]': hasBarColor,
});

export const heading = 'rs-mb-3 ml-22 whitespace-pre-line mt-auto';
export const body = '*:*:leading-snug';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a WYSIWYG 🤣

export const cta = 'rs-mt-2';
93 changes: 93 additions & 0 deletions components/DataCard/DataCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { cnb } from 'cnbuilder';
import { AnimateInView, type AnimationType } from '@/components/Animate';
import { NumberCounter } from '@/components/NumberCounter';
import { Container } from '@/components/Container';
import { FlexBox } from '@/components/FlexBox';
import { Heading, type HeadingType } from '../Typography';
import { accentBorderColors, type AccentBorderColorType, type PaddingType } from '@/utilities/datasource';
import { splitNumberString } from '@/utilities/splitNumberString';
import * as styles from './DataCard.styles';

export type DataCardProps = React.HTMLAttributes<HTMLDivElement> & {
heading?: string;
headingLevel?: HeadingType;
isDarkTheme?: boolean;
barColor?: AccentBorderColorType;
body?: React.ReactNode;
paddingTop?: PaddingType;
cta?: React.ReactNode;
isCounter?: boolean;
// In number of seconds
counterDuration?: number;
animation?: AnimationType;
delay?: number;
};

export const DataCard = ({
heading,
headingLevel = 'h3',
barColor,
body,
cta,
paddingTop,
isDarkTheme,
isCounter,
counterDuration,
animation = 'slideUp',
delay,
children,
className,
...props
}: DataCardProps) => {
const headingProcessed = isCounter ? splitNumberString(heading) : undefined;

return (
<AnimateInView animation={animation} delay={delay} className={styles.animateWrapper}>
<Container
as="article"
width="full"
pt={paddingTop}
className={styles.root}
{...props}
>
<FlexBox direction="col" className={styles.flex}>
{/* If number counter is enabled, aria-hidden the animated heading and add a SR only heading */}
{isCounter && heading && (
<Heading as={headingLevel} srOnly>{heading}</Heading>
)}
Comment on lines +54 to +57
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is a number counter in the heading, add a SR only heading with all static heading, then aria-hidden the visible heading with the number counter.

{heading && (
<Heading
as={headingLevel}
font="druk"
leading="druk"
color={isDarkTheme ? 'white' : 'black'}
size="f5"
aria-hidden={isCounter}
className={styles.heading}
>
{isCounter ? (
<>
{headingProcessed?.beforeNumber}
<NumberCounter number={headingProcessed?.number} duration={counterDuration} />
{headingProcessed?.afterNumber}
</>
) : (
heading
)}
</Heading>
)}
<div className={cnb(styles.content(!!barColor), accentBorderColors[barColor])}>
<div className={styles.body}>
{body}
</div>
{!!cta && (
<div className={styles.cta}>
{cta}
</div>
)}
</div>
</FlexBox>
</Container>
</AnimateInView>
);
};
1 change: 1 addition & 0 deletions components/DataCard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './DataCard';
4 changes: 2 additions & 2 deletions components/Homepage/IdealFellow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export const IdealFellow = () => {
</div>
<Heading size="base" color="white" className="absolute bottom-300 ml-[20vw]">
<Text as="span" font="druk" size="f8" leading="display" className="block">Shape</Text>
<Text as="span" font="serif" weight="semibold" italic size="f9" className="leading-[0.9] block">what’s</Text>
<Text as="span" font="serif" weight="semibold" italic size="f9" className="leading-[0.9] block">next</Text>
<Text as="span" font="serif" weight="semibold" italic size="f9" leading="druk" className="block">what’s</Text>
<Text as="span" font="serif" weight="semibold" italic size="f9" leading="druk" className="block">next</Text>
</Heading>
<div className="group hover:backdrop-blur-sm transition-all absolute bottom-200 right-0 bg-black-50/50 backdrop-blur-xl w-5/12 rs-px-4 rs-pt-4 rs-pb-1">
<Text variant="caption" color="white" weight="semibold" className="mb-04em">Preparing citizens</Text>
Expand Down
2 changes: 1 addition & 1 deletion components/MomentPoster/MomentPoster.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const overlay = (hasBgGradient?: boolean) => cnb('absolute top-0 left-0 w

export const contentWrapper = 'lg:rs-pr-9 ml-0';

export const heading = 'leading-[0.9] max-w-1000 mx-auto rs-mb-1';
export const heading = 'max-w-1000 mx-auto rs-mb-1';
export const headingWrapper = 'mx-auto w-fit gap-02em';

export const thumbnailWrapper = 'inline-block';
Expand Down
2 changes: 1 addition & 1 deletion components/MomentPoster/MomentPoster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const MomentPoster = ({
/>
)}
<Container className={styles.wrapper}>
<Heading as="h2" size="splash" font="druk" align="center" className={styles.heading}>
<Heading as="h2" size="splash" font="druk" leading="druk" align="center" className={styles.heading}>
<FlexBox as="span" alignItems="baseline" className={styles.headingWrapper}>
{textBefore && (
<AnimateInView animation="slideInFromLeft">
Expand Down
12 changes: 4 additions & 8 deletions components/NumberCounter.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { m, useInView } from 'framer-motion';
import { Text } from './Typography';

/**
* TODO: This is a POC. Will add types and props if client decides we need this.
*/
type NumberCounterProps = {
number: number;
duration?: number;
afterText?: string;
};

export const NumberCounter = ({ number, duration = 2500, afterText = '' }: NumberCounterProps) => {
export const NumberCounter = ({ number, duration = 2.5 }: NumberCounterProps) => {
const [count, setCount] = useState(1);
const ref = useRef(null);
const isInView = useInView(ref);
const interval = duration / number;
// Multiply duration by 1000 to convert to milliseconds
const interval = Math.floor(duration * 1000 / number);

useEffect(() => {
if (isInView && count < number) {
Expand All @@ -28,7 +24,7 @@ export const NumberCounter = ({ number, duration = 2500, afterText = '' }: Numbe

return (
<m.span animate={{ opacity: count > 0 ? 1 : 0 }} ref={ref}>
<Text size={8} weight="bold" align="center" className="text-robins-egg">{count}{afterText}</Text>
{count}
</m.span>
);
};
69 changes: 69 additions & 0 deletions components/Storyblok/SbDataCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { storyblokEditable, type SbBlokData } from '@storyblok/react/rsc';
import { type StoryblokRichtext } from 'storyblok-rich-text-react-renderer-ts';
import { CreateBloks } from '@/components/CreateBloks';
import { DataCard } from '@/components/DataCard';
import { RichText } from '@/components/RichText';
import { type AnimationType } from '@/components/Animate';
import { type HeadingType } from '@/components/Typography';
import { paletteAccentColors, type PaletteAccentHexColorType } from '@/utilities/colorPalettePlugin';
import { type PaddingType } from '@/utilities/datasource';
import { getNumBloks } from '@/utilities/getNumBloks';
import { hasRichText } from '@/utilities/hasRichText';

export type SbDataCardProps = {
blok: {
_uid: string;
heading?: string;
headingLevel?: HeadingType;
isSmallHeading?: boolean;
superhead?: string;
body: StoryblokRichtext;
cta?: SbBlokData[];
paddingTop?: PaddingType;
isDarkTheme?: boolean;
isCounter?: boolean;
counterDuration?: number;
barColor?: {
value?: PaletteAccentHexColorType;
}
animation?: AnimationType;
delay?: number;
};
};

export const SbDataCard = ({
blok: {
heading,
headingLevel,
body,
cta,
paddingTop,
isDarkTheme,
isCounter,
counterDuration,
barColor: { value } = {},
animation,
delay,
},
blok,
}: SbDataCardProps) => {
const Body = hasRichText(body) ? <RichText wysiwyg={body} textColor={isDarkTheme ? 'white' : 'black'} /> : undefined;
const Cta = !!getNumBloks(cta) ? <CreateBloks blokSection={cta} /> : undefined;

return (
<DataCard
{...storyblokEditable(blok)}
heading={heading}
headingLevel={headingLevel}
isDarkTheme={isDarkTheme}
isCounter={isCounter}
counterDuration={counterDuration}
barColor={paletteAccentColors[value]}
cta={Cta}
body={Body}
paddingTop={paddingTop}
animation={animation}
delay={delay}
/>
);
};
2 changes: 2 additions & 0 deletions components/StoryblokProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SbBlurryPoster } from '@/components/Storyblok/SbBlurryPoster';
import { SbCardWysiwyg } from '@/components/Storyblok/SbCardWysiwyg';
import { SbChangemakerCard } from '@/components/Storyblok/SbChangemakerCard';
import { SbCta } from '@/components/Storyblok/SbCta';
import { SbDataCard } from '@/components/Storyblok/SbDataCard';
import { SbEmbedMedia } from '@/components/Storyblok/SbEmbedMedia';
import { SbGrid } from '@/components/Storyblok/SbGrid';
import { SbGridAlternating } from '@/components/Storyblok/SbGridAlternating';
Expand Down Expand Up @@ -36,6 +37,7 @@ export const components = {
sbCardWysiwyg: SbCardWysiwyg,
sbChangemakerCard: SbChangemakerCard,
sbCta: SbCta,
sbDataCard: SbDataCard,
sbEmbedMedia: SbEmbedMedia,
sbGrid: SbGrid,
sbGridAlternating: SbGridAlternating,
Expand Down
2 changes: 1 addition & 1 deletion components/Temporary/DemoContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const DemoContent = () => (
<Heading size={7} font="sans">Animated counters</Heading>
<Grid md={3} gap="card" alignItems="center" justifyItems="center">
<NumberCounter number={42} />
<NumberCounter number={9} afterText="K+" />
<NumberCounter number={9} />
<NumberCounter number={120} />
</Grid>
</Container>
Expand Down
7 changes: 4 additions & 3 deletions components/Typography/typography.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const fontLeadings = {
cozy: 'leading-cozy', // 1.4
normal: 'leading', // 1.5
trim: 'leading-trim', // 0.75
druk: 'leading-druk', // 0.9
};

export const textAligns = {
Expand Down Expand Up @@ -71,15 +72,15 @@ export const textVariants = {
big: 'big-paragraph',
subheading: 'subheading',
/**
* Campaign typography styles
* (-gc ones are Decanter styles with Campaign modifications)
* Momentum typography styles
* (-gc ones are Decanter styles with Momentum modifications)
*/
caption: 'caption',
card: 'gc-card',
changemaker: 'gc-changemaker',
intro: 'gc-intro-text',
/**
* Campaign only styles
* Momentum only styles
* No gc- prefix because no Decanter equivalent
*/
overview: 'overview',
Expand Down
3 changes: 2 additions & 1 deletion tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ export default {
],
theme: {
containers: require(`${dir}/theme/gc-containers.js`)(),
// Campaign themes extending our Decanter ones
// Momentum themes extending our Decanter ones
extend: {
colors: require(`${dir}/theme/gc-colors.js`)(),
fontFamily: require(`${dir}/theme/gc-fontFamily.js`)(),
lineHeight: require(`${dir}/theme/gc-lineHeight.js`)(),
screens: require(`${dir}/theme/gc-screens.js`)(),
},
},
Expand Down
4 changes: 2 additions & 2 deletions tailwind/plugins/base/gc-base.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/**
* Campaign custom base styles extending Decanter 7 base
* Momentum custom base styles extending Decanter 7 base
*/

module.exports = function () {
return function ({ addBase, config }) {
addBase({
html: {
overflowY: 'visible !important', // Need this for sticky nav to work
color: '#17171A', // Campaign black
color: '#17171A', // Momentum black
},
body: {
fontSize: '1.8rem',
Expand Down
2 changes: 1 addition & 1 deletion tailwind/plugins/components/gc-typography.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Campaign specific typography styles
* Momentum specific typography styles
*/
module.exports = function () {
return function ({ addComponents, theme }) {
Expand Down
2 changes: 1 addition & 1 deletion tailwind/plugins/theme/gc-colors.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Giving Campaign colors
* Momentum colors
*/
module.exports = function () {
return {
Expand Down
2 changes: 1 addition & 1 deletion tailwind/plugins/theme/gc-fontFamily.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Giving Campaign fonts
* Momentum fonts
*/
module.exports = function () {
return {
Expand Down
9 changes: 9 additions & 0 deletions tailwind/plugins/theme/gc-lineHeight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Momentum line heights
*/
module.exports = function () {
return {
// Extra tight line height for Druk font in some components, e.g., Data Card, Moment Poster
druk: '0.9',
};
};
Comment on lines +1 to +9
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add extra tight leading utility for Druk font that are used in a few places

23 changes: 23 additions & 0 deletions utilities/splitNumberString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Separates a string into three parts: text before the number, the number itself, and text after the number.
*
* @param str The input string containing a number.
* @returns An object with three properties: beforeNumber, number, and afterNumber.
*/
export const splitNumberString = (str: string): { beforeNumber: string; number?: number; afterNumber: string } => {
const numberRegex = /\d+/;
const numberMatch = str.match(numberRegex);

if (numberMatch) {
const number = parseFloat(numberMatch[0]);
const numberIndex = numberMatch.index ?? 0;
const beforeNumber = str.substring(0, numberIndex);
const afterNumber = str.substring(numberIndex + (numberMatch[0]?.length ?? 0));

return {
beforeNumber: beforeNumber ?? '',
number: number,
afterNumber: afterNumber ?? '',
};
}
};
Loading