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 (
+
+
+
+
+
+
+
+ {backgroundImage?.url ? (
+
+ ) : null}
+
+
+ {logo?.url ? (
+
+
+
+ ) : 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' |