diff --git a/src/components/Moments/IdBadgeMoment/IdBadgeMoment.stories.ts b/src/components/Moments/IdBadgeMoment/IdBadgeMoment.stories.ts new file mode 100644 index 0000000..ce42169 --- /dev/null +++ b/src/components/Moments/IdBadgeMoment/IdBadgeMoment.stories.ts @@ -0,0 +1,110 @@ +import { momentStoryBookDecorator } from '../../../stories/decorators'; +import IdBadgeMoment from './IdBadgeMoment'; + +const momentData = { + index: 0, + type: 'idBadge', + title: 'ID Badge Moment Example', + subtitle: 'Employment History', + icon: { name: 'badge', type: 'mui' }, + color: { background: '#4a90e2', text: '#ffffff' }, + label: 'Badges', + data: { + badges: [ + { + backgroundImage: { url: 'https://upload.wikimedia.org/wikipedia/commons/5/56/Aerial_view_of_Apple_Park_dllu.jpg', alt: 'Apple HQ' }, + logo: { url: 'https://upload.wikimedia.org/wikipedia/commons/f/fa/Apple_logo_black.svg', alt: 'Apple Logo' }, + content: { + blocks: [ + { type: 'TEXT', text: 'Software Engineer', id: 'c1' }, + { type: 'TEXT', text: 'Start: Jan 2021 – End: Present', id: 'c2' }, + ], + caption: { + blocks: [ + { type: 'TEXT', text: 'Apple Inc.', id: 'company-name' }, + { type: 'TEXT', text: 'Innovative tech company', id: 'company-desc' }, + { type: 'BUTTON', button: { label: 'Learn More', url: 'https://apple.com' } }, + ], + }, + }, + }, + { + backgroundImage: { url: 'https://upload.wikimedia.org/wikipedia/commons/5/5f/Aerial_Microsoft_West_Campus_August_2009.jpg', alt: 'Microsoft HQ' }, + logo: { + url: 'https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg', + alt: 'Microsoft Logo', + }, + content: { + blocks: [ + { type: 'TEXT', text: 'Product Manager', id: 'c3' }, + { type: 'TEXT', text: 'Start: Mar 2018 – End: Dec 2020', id: 'c4' }, + ], + caption: { + blocks: [ + { type: 'TEXT', text: 'Microsoft', id: 'company-name' }, + { type: 'TEXT', text: 'Empowering every person and organization on the planet to achieve more', id: 'company-desc' }, + { type: 'BUTTON', button: { label: 'Learn More', url: 'https://microsoft.com' } }, + ], + }, + }, + }, + { + logo: { + url: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/Google_2015_logo.svg', + alt: 'Google Logo', + }, + content: { + blocks: [ + { type: 'TEXT', text: 'UX Designer', id: 'c5' }, + { type: 'TEXT', text: 'Start: Feb 2016 – End: Feb 2018', id: 'c6' }, + ], + caption: { + blocks: [ + { type: 'TEXT', text: 'Google', id: 'company-name' }, + { type: 'TEXT', text: 'Organizing the world’s information and making it universally accessible.', id: 'company-desc' }, + { type: 'BUTTON', button: { label: 'Learn More', url: 'https://google.com' } }, + ], + }, + }, + }, + { + logo: { + url: 'https://upload.wikimedia.org/wikipedia/commons/0/05/Facebook_Logo_%282019%29.png', + alt: 'Meta Logo', + }, + content: { + blocks: [ + { type: 'TEXT', text: 'Data Scientist', id: 'c7' }, + { type: 'TEXT', text: 'Start: May 2014 – End: Jan 2016', id: 'c8' }, + ], + caption: { + blocks: [ + { type: 'TEXT', text: 'Meta (Facebook)', id: 'company-name' }, + { type: 'TEXT', text: 'Building technologies that help people connect, find communities, and grow businesses.', id: 'company-desc' }, + { type: 'BUTTON', button: { label: 'Learn More', url: 'https://meta.com' } }, + ], + }, + }, + }, + ], + layout: 'zigzag', + }, +}; + +export default { + title: 'Moments/IdBadge Moment', + component: IdBadgeMoment, + decorators: [momentStoryBookDecorator], + parameters: { + deepControls: { enabled: true }, + layout: 'centered', + }, + tags: ['autodocs'], +}; + +export const DefaultIdBadgeMoment = { + name: 'Default ID Badge', + args: { + moment: momentData, + }, +}; diff --git a/src/components/Moments/IdBadgeMoment/IdBadgeMoment.tsx b/src/components/Moments/IdBadgeMoment/IdBadgeMoment.tsx new file mode 100644 index 0000000..cd82d44 --- /dev/null +++ b/src/components/Moments/IdBadgeMoment/IdBadgeMoment.tsx @@ -0,0 +1,30 @@ +import { observer } from 'mobx-react-lite'; + +import IdBadge from '../../UI/IdBadge/IdBadge'; +import CardsBaseMoment from '../CardsBaseMoment/CardsBaseMoment'; +import type { IdBadgeMomentProps } from './IdBadgeMoment.types'; + +const IdBadgeMoment = observer(({ moment }: IdBadgeMomentProps) => { + const badges = moment.items ?? []; + + if (badges.length === 0) return null; + + return ( + + {badges.map((badge, index) => ( + + ))} + + + ); +}); + +export default IdBadgeMoment; diff --git a/src/components/Moments/IdBadgeMoment/IdBadgeMoment.types.ts b/src/components/Moments/IdBadgeMoment/IdBadgeMoment.types.ts new file mode 100644 index 0000000..e871957 --- /dev/null +++ b/src/components/Moments/IdBadgeMoment/IdBadgeMoment.types.ts @@ -0,0 +1,4 @@ +import type IdBadgeMomentStore from '../../../state/moments/idBadgeMomentStore'; +import type { BaseMomentPropsWithoutChildren } from '../BaseMoment/BaseMoment.types'; + +export type IdBadgeMomentProps = BaseMomentPropsWithoutChildren; diff --git a/src/components/UI/Cards/Cards.tsx b/src/components/UI/Cards/Cards.tsx index 4caae82..b24f17b 100644 --- a/src/components/UI/Cards/Cards.tsx +++ b/src/components/UI/Cards/Cards.tsx @@ -8,6 +8,7 @@ import CardsGridLayout from './CardsGridLayout'; import CardsOrbitLayout from './CardsOrbitLayout'; import CardsRowLayout from './CardsRowLayout'; import CardsStackLayout from './CardsStackLayout'; +import CardsZigZagLayout from './CardsZigZagLayout'; const Cards = observer((props: CardsProps) => { const { layout = 'grid' } = props; @@ -37,6 +38,10 @@ const Cards = observer((props: CardsProps) => { + + + + ); }); diff --git a/src/components/UI/Cards/CardsZigZagLayout.styles.ts b/src/components/UI/Cards/CardsZigZagLayout.styles.ts new file mode 100644 index 0000000..eca312c --- /dev/null +++ b/src/components/UI/Cards/CardsZigZagLayout.styles.ts @@ -0,0 +1,111 @@ +const styles = { + zigZagLayoutRoot: { + display: 'flex', + flexDirection: 'column', + width: '100%', + }, + + zigZagSection: { + position: 'relative', + minHeight: 200, + '&:hover': { boxShadow: 'none' }, + }, + + zigZagSectionLeft: { + backgroundColor: 'background.default', + color: 'text.primary', + }, + + zigZagSectionRight: { + backgroundColor: 'primary.main', + color: 'primary.contrastText', + }, + + angle: { + backgroundColor: 'inherit', + height: '25%', + left: 0, + position: 'absolute', + right: 0, + top: 0, + transformOrigin: 'top left', + zIndex: 0, + }, + + angleLeft: { + transform: 'skewY(-6deg)', + }, + + angleRight: { + transform: 'skewY(6deg)', + transformOrigin: 'top right', + }, + + angledSectionBackground: { + marginTop: '-50px', + paddingTop: 100, + paddingBottom: 250, + position: 'relative', + width: '100%', + zIndex: 1, + }, + + zigZagGrid: { + marginTop: '-25px', + position: 'relative', + zIndex: 1, + }, + + zigZagCard: { + alignItems: 'center', + display: 'flex', + justifyContent: 'center', + maxWidth: 400, + transformStyle: 'preserve-3d', + transition: 'transform 0.4s ease, box-shadow 0.4s ease', + }, + + zigZagCaption: { + container: { + textAlign: 'center', + width: '100%', + marginBottom: '12px', + '& *': { + color: 'inherit', + }, + }, + textBlock: { + '&:nth-of-type(1)': { + fontSize: '1.25rem', + fontWeight: 600, + lineHeight: 1.2, + marginBottom: '6px', + }, + '&:nth-of-type(n+2)': { + fontSize: '0.95rem', + fontWeight: 400, + lineHeight: 1.4, + marginBottom: '8px', + }, + }, + buttonBlock: { + backgroundColor: 'primary.main', + border: '1px solid', + borderColor: 'primary.main', + borderRadius: 4, + cursor: 'pointer', + display: 'inline-block', + fontSize: '0.85rem', + fontWeight: 500, + padding: '6px 16px', + textAlign: 'center', + textDecoration: 'none', + transition: 'background-color 0.3s ease, color 0.3s ease', + '&:hover': { + backgroundColor: 'primary.dark', + }, + }, + }, +}; + +export default styles; diff --git a/src/components/UI/Cards/CardsZigZagLayout.tsx b/src/components/UI/Cards/CardsZigZagLayout.tsx new file mode 100644 index 0000000..c845721 --- /dev/null +++ b/src/components/UI/Cards/CardsZigZagLayout.tsx @@ -0,0 +1,120 @@ +import Box from '@mui/material/Box'; +import Grid from '@mui/material/Grid2'; +import React, { isValidElement } from 'react'; + +import { deepMerge } from '../../../utils/object'; +import type { CardsLayoutProps } from './Cards.types'; +import CardsItem from './CardsItem'; +import styles from './CardsZigZagLayout.styles'; + +export default function CardsZigZagLayout({ + children, + keyFn, + sx, +}: CardsLayoutProps) { + const cards = React.Children.toArray(children); + + type CaptionBlock = + | { id: string; type: 'TEXT'; text: string } + | { id: string; type: 'BUTTON'; button: { collection_id?: number; label?: string } }; + + return ( + + {cards.map((card, index) => { + const isEven = index % 2 === 0; + + const caption = isValidElement(card) + && card.props?.content?.caption + && Array.isArray(card.props.content.caption.blocks) + ? card.props.content.caption + : undefined; + + const renderCaption = caption ? ( + + {caption.blocks.map((block: CaptionBlock) => { + if (block.type === 'TEXT' && 'text' in block) { + return ( + + {block.text} + + ); + } + if (block.type === 'BUTTON' && 'button' in block) { + return ( + + {block.button.label} + + ); + } + return null; + })} + + ) : null; + + return ( + + + + + {isEven ? ( + <> + {renderCaption} + + + + {card} + + + + ) : ( + <> + + + {card} + + + + {renderCaption} + + )} + + + ); + })} + + ); +} diff --git a/src/components/UI/IdBadge/IdBadge.styles.ts b/src/components/UI/IdBadge/IdBadge.styles.ts new file mode 100644 index 0000000..1f1a4bb --- /dev/null +++ b/src/components/UI/IdBadge/IdBadge.styles.ts @@ -0,0 +1,91 @@ +const styles = { + container: { + cursor: 'pointer', + margin: 25, + perspective: 800, + }, + + badge: { + backgroundColor: 'rgba(0,0,0,0.7)', + borderRadius: 8, + boxShadow: + 'rgba(0,0,0,0.6) 0 30px 60px 0, inset #333 0 0 0 5px, inset rgba(255,255,255,0.2) 0 0 0 6px', + color: '#fff', + height: 320, + margin: '0 auto', + overflow: 'hidden', + position: 'relative', + width: 240, + transition: 'transform 0.6s ease, box-shadow 0.6s ease', + '&:hover': { + transform: 'translateY(-10px) scale(1.03) rotateX(2deg)', + '& .badge-dates': { + transform: 'translateY(0)', + opacity: 1, + }, + }, + + '& .badge-inner': { + borderRadius: 8, + height: '100%', + position: 'relative', + width: '100%', + }, + + '& .badge-inner__bg': { + backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + backgroundSize: 'cover', + height: '100%', + left: 0, + opacity: 0.5, + pointerEvents: 'none', + position: 'absolute', + top: 0, + width: '100%', + }, + + '& .badge-body': { + bottom: 15, + position: 'absolute', + textAlign: 'center', + width: '100%', + zIndex: 2, + }, + + '& .badge-logo img': { + background: 'transparent', + borderRadius: 0, + display: 'block', + margin: '0 auto 5px', + maxHeight: 80, + maxWidth: '90%', + }, + + '& .badge-dates': { + color: '#fff', + fontSize: '0.70rem', + opacity: 0, + padding: '0 4px', + textAlign: 'center', + transform: 'translateY(100%)', + transition: 'transform 0.5s ease, opacity 0.5s ease', + width: '100%', + }, + }, + + topGraphic: { + left: '50%', + position: 'absolute', + top: -50, + transform: 'translateX(-50%)', + zIndex: 1, + '& img': { + display: 'block', + width: '100%', + height: 'auto', + }, + }, +}; + +export default styles; diff --git a/src/components/UI/IdBadge/IdBadge.tsx b/src/components/UI/IdBadge/IdBadge.tsx new file mode 100644 index 0000000..fab0699 --- /dev/null +++ b/src/components/UI/IdBadge/IdBadge.tsx @@ -0,0 +1,73 @@ +import Box from '@mui/material/Box'; + +import type { ButtonContentBlock } from '../../../types'; +import { Content } from '../Content'; +import styles from './IdBadge.styles'; +import type { IdBadgeProps } from './IdBadge.types'; + +export default function IdBadge({ + backgroundImage, + logo, + content, + information, +}: IdBadgeProps) { + const buttonBlocks = information?.blocks.filter( + (b): b is ButtonContentBlock => b.type === 'BUTTON' && 'button' in b, + ); + + return ( + + + ID badge clip graphic + + + + + {backgroundImage?.url ? ( + + ) : null} + + + {logo?.url ? ( + + {logo.alt + + ) : null} + + {content ? ( + + + + ) : null} + + {buttonBlocks && buttonBlocks.length > 0 ? ( + + {buttonBlocks.map((block) => { + const href = 'collection_id' in block.button + ? `/collections/${block.button.collection_id}` + : '#'; + return ( + + {block.button.label} + + ); + })} + + ) : null} + + + + + ); +} diff --git a/src/components/UI/IdBadge/IdBadge.types.ts b/src/components/UI/IdBadge/IdBadge.types.ts new file mode 100644 index 0000000..be32cfb --- /dev/null +++ b/src/components/UI/IdBadge/IdBadge.types.ts @@ -0,0 +1,3 @@ +import type { IdBadge } from '../../../types'; + +export type IdBadgeProps = IdBadge; diff --git a/src/configs/momentConfig.ts b/src/configs/momentConfig.ts index 0cb58af..baadd88 100644 --- a/src/configs/momentConfig.ts +++ b/src/configs/momentConfig.ts @@ -5,6 +5,7 @@ import GalleryMomentStore from '../state/moments/galleryMomentStore'; import GeoMomentStore from '../state/moments/geoMapMomentStore'; import HathiTrustMomentStore from '../state/moments/hathiTrustMomentStore'; import HTMLMomentStore from '../state/moments/htmlMomentStore'; +import IdBadgeMomentStore from '../state/moments/idBadgeMomentStore'; import IFrameMomentStore from '../state/moments/iframeMomentStore'; import ImageMomentStore from '../state/moments/imageMomentStore'; import LibraryMomentStore from '../state/moments/libraryMomentStore'; @@ -62,6 +63,11 @@ const MomentConfigMap: Record = { icon: { name: 'code', type: 'mui' }, store: HTMLMomentStore.build, }, + idBadge: { + component: 'IdBadgeMoment', + icon: { name: 'badge', type: 'mui' }, + store: IdBadgeMomentStore.build, + }, iframe: { component: 'IFrameMoment', icon: { name: 'language', type: 'mui' }, diff --git a/src/state/moments/idBadgeMomentStore.ts b/src/state/moments/idBadgeMomentStore.ts new file mode 100644 index 0000000..e273613 --- /dev/null +++ b/src/state/moments/idBadgeMomentStore.ts @@ -0,0 +1,31 @@ +import { + makeObservable, + override, +} from 'mobx'; + +import type { IdBadge, IdBadgeMomentData, Moment } from '../../types'; +import type MomentsStore from '../momentsStore'; +import CardsBaseMomentStore from './cardsBaseMomentStore'; + +export default class IdBadgeMomentStore extends CardsBaseMomentStore { + constructor(moments: MomentsStore, moment: Moment) { + super(moments, moment); + + makeObservable(this, { + items: override, + layout: override, + }); + } + + get items(): IdBadge[] { + return this.data?.badges ?? []; + } + + get layout() { + return this.data.layout || 'zigzag'; + } + + static build(moments: MomentsStore, moment: Moment) { + return new IdBadgeMomentStore(moments, moment); + } +} diff --git a/src/types.ts b/src/types.ts index 1ee45a1..acaeb09 100644 --- a/src/types.ts +++ b/src/types.ts @@ -78,7 +78,7 @@ export type Caption = AtLeastOne<{ readonly content?: string; }>; -export type CardsLayout = 'carousel' | 'grid' | 'orbit' | 'row' | 'stack'; +export type CardsLayout = 'carousel' | 'grid' | 'orbit' | 'row' | 'stack' | 'zigzag'; export type CardsLayoutOptions = { // Carousel Layout Options: @@ -130,6 +130,7 @@ export type ColorString = ThemeColorOption | ColorHex; export type Content = { readonly blocks: ContentBlock[]; + readonly caption?: Content; readonly id?: string; readonly sx?: SxProps; }; @@ -252,6 +253,17 @@ export type HTMLMomentData = { export type Icon = ImageIcon | MuiIcon | NoIcon; +export type IdBadge = { + readonly backgroundImage?: Image; + readonly content?: Content; + readonly information?: Content; + readonly logo?: Image; +}; + +export type IdBadgeMomentData = CardsBaseMomentData & { + readonly badges: IdBadge[]; +}; + export type IFrameMomentData = { readonly iframe: { readonly fit?: MomentContentFit; @@ -372,6 +384,7 @@ export type MomentData = CardsContentMomentData | GalleryMomentData | GeoMapMomentData | + IdBadgeMomentData | ImageMomentData | IFrameMomentData | HathiTrustMomentData | @@ -409,6 +422,7 @@ export type MomentType = 'gallery' | 'hathiTrust' | 'html' | + 'idBadge' | 'iframe' | 'image' | 'library' |