diff --git a/components/AuthorAvatars.tsx b/components/AuthorAvatars.tsx index 3ffb1be4207..03bf39dcb44 100644 --- a/components/AuthorAvatars.tsx +++ b/components/AuthorAvatars.tsx @@ -23,7 +23,9 @@ export default function AuthorAvatars({ authors = [] }: AuthorAvatarsProps) { 0 ? `left- absolute${index * 7} top-0` : `mr- relative${(authors.length - 1) * 7}`} z-${(authors.length - 1 - index) * 10} size-10 rounded-full border-2 border-white object-cover hover:z-50`} + className={`${index > 0 ? `left- absolute${index * 7} top-0` : `mr- relative${(authors.length - 1) * 7}`} + z-${(authors.length - 1 - index) * 10} size-10 rounded-full border-2 + border-white object-cover hover:z-50`} src={author.photo} loading='lazy' data-testid='AuthorAvatars-img' diff --git a/components/Caption.tsx b/components/Caption.tsx index 043db0b7a16..cae2cafd770 100644 --- a/components/Caption.tsx +++ b/components/Caption.tsx @@ -1,14 +1,14 @@ import React from 'react'; interface CaptionProps { - children: React.ReactNode; + children: string; } /** * @description This component displays textual captions. * * @param {CaptionProps} props - The props for the Caption component. - * @param {React.ReactNode} props.children - The content to be displayed as the caption. + * @param {string} props.children - The content to be displayed as the caption. */ export default function Caption({ children }: CaptionProps) { return ( diff --git a/components/Figure.tsx b/components/Figure.tsx new file mode 100644 index 00000000000..878258ce775 --- /dev/null +++ b/components/Figure.tsx @@ -0,0 +1,45 @@ +import { Float } from '@/types/components/FigurePropsType'; + +import Caption from './Caption'; + +interface FigureProps { + src: string; + caption: string; + widthClass: string; + className: string; + float: Float; + altOnly: string; + imageClass?: string; +} + +/** + * This component displays a figure with image and a caption. + * @param {FigureProps} props - The props for the Figure component + * @param {string} props.src - The source URL for the image + * @param {string} props.caption - The caption for the image + * @param {string} props.widthClass - Additional width classes for the figure + * @param {string} props.className - Additional classes for the figure + * @param {Float} props.float - The float direction of the figure (Float.LEFT or Float.RIGHT) + * @param {string} props.altOnly - The alternative text for the image if caption is not provided + * @param {string} props.imageClass - Additional classes for the image + */ +export default function Figure({ src, caption, widthClass, className, float, altOnly, imageClass = '' }: FigureProps) { + const alt = altOnly || caption; + + let floatClassNames = ''; + + if (float === Float.LEFT) { + floatClassNames = 'mr-4 float-left'; + } else if (float === Float.RIGHT) { + floatClassNames = 'ml-4 float-right'; + } + + return ( +
+
+ {alt} + {caption && {caption}} +
+
+ ); +} diff --git a/components/Head.tsx b/components/Head.tsx new file mode 100644 index 00000000000..65b8861c5c8 --- /dev/null +++ b/components/Head.tsx @@ -0,0 +1,88 @@ +import Head from 'next/head'; +import { useContext } from 'react'; +import ReactGA from 'react-ga'; +import TagManager from 'react-gtm-module'; + +import AppContext from '../context/AppContext'; + +interface IHeadProps { + title: string; + description?: string; + image: string; + rssTitle?: string; + rssLink?: string; +} + +/** + * @param {string} props.title - The title of the page + * @param {string} props.description - The description of the page + * @param {string} props.image - The image of the page + * @param {string} props.rssTitle - The RSS title of the page + * @param {string} props.rssLink - The RSS link of the page + * @description The head of the page with the meta tags + */ +export default function HeadComponent({ + title, + description = `Open source tools to easily build and maintain your event-driven architecture. + All powered by the AsyncAPI specification, the industry standard for defining asynchronous APIs.`, + image = '/img/social/website-card.jpg', + rssTitle = 'RSS Feed for AsyncAPI Initiative Blog', + rssLink = '/rss.xml' +}: IHeadProps) { + const url = process.env.DEPLOY_PRIME_URL || process.env.DEPLOY_URL || 'http://localhost:3000'; + const appContext = useContext(AppContext); + const { path = '' } = appContext || {}; + let currImage = image; + + const permalink = `${url}${path}`; + let type = 'website'; + + if (path.startsWith('/docs') || path.startsWith('/blog')) { + type = 'article'; + } + + if (!image.startsWith('http') && !image.startsWith('https')) { + currImage = `${url}${image}`; + } + + const permTitle = 'AsyncAPI Initiative for event-driven APIs'; + + const currTitle = title ? `${title} | ${permTitle}` : permTitle; + + // enable google analytics + if (typeof window !== 'undefined' && window.location.hostname.includes('asyncapi.com')) { + TagManager.initialize({ gtmId: 'GTM-T58BTVQ' }); + ReactGA.initialize('UA-109278936-1'); + ReactGA.pageview(window.location.pathname + window.location.search); + } + + return ( + + + + + + + + {/* Google / Search Engine Tags */} + + + + + {/* Twitter Card data */} + + + + + + {/* Open Graph data */} + + + + + + + {currTitle} + + ); +} diff --git a/components/Loader.tsx b/components/Loader.tsx new file mode 100644 index 00000000000..09324174ec2 --- /dev/null +++ b/components/Loader.tsx @@ -0,0 +1,24 @@ +import { twMerge } from 'tailwind-merge'; + +interface LoaderProps { + className?: string; + dark?: boolean; +} + +/** + * This component displays a loader. + * @param {LoaderProps} props - The props for the Loader component + * @param {string} props.className - Additional classes for the loader + * @param {boolean} props.dark - Whether the loader should be in dark mode + */ +export default function Loader({ className = '', dark = false }: LoaderProps) { + return ( +
+ +
Waiting for response...
+
+ ); +} diff --git a/components/OpenAPIComparison.tsx b/components/OpenAPIComparison.tsx new file mode 100644 index 00000000000..3d0fa271a90 --- /dev/null +++ b/components/OpenAPIComparison.tsx @@ -0,0 +1,274 @@ +import React, { useState } from 'react'; + +interface HoverState { + Info: boolean; + Servers: boolean; + Paths: boolean; + PathItem: boolean; + Summary: boolean; + Operation: boolean; + Message: boolean; + Tags: boolean; + External: boolean; + Components: boolean; +} + +interface OpenAPIComparisonProps { + className?: string; +} + +/** + * @description React component for comparing OpenAPI 3.0 and AsyncAPI 2.0. + * @param {string} [props.className=''] - Additional CSS classes for styling. + */ +export default function OpenAPIComparison({ className = '' }: OpenAPIComparisonProps) { + const [hoverState, setHoverState] = useState({ + Info: false, + Servers: false, + Paths: false, + PathItem: true, + Summary: false, + Operation: false, + Message: false, + Tags: false, + External: false, + Components: false + }); + + return ( +
+
+

OpenAPI 3.0

+
+
setHoverState((prevState) => ({ ...prevState, Info: true }))} + onMouseLeave={() => setHoverState({ ...hoverState, Info: false })} + data-testid='OpenAPI-sec-info' + > + Info +
+
+
setHoverState((prevState) => ({ ...prevState, Servers: true }))} + onMouseLeave={() => setHoverState({ ...hoverState, Servers: false })} + data-testid='OpenAPI-sec-servers' + > + Servers +
+
Security
+
+
setHoverState((prevState) => ({ ...prevState, Paths: true }))} + onMouseLeave={() => setHoverState({ ...hoverState, Paths: false })} + data-testid='OpenAPI-paths' + > + Paths +
+
{ + return setHoverState((prevState) => ({ ...prevState, PathItem: true })); + }} + onMouseLeave={() => { + return setHoverState({ ...hoverState, PathItem: false }); + }} + data-testid='OpenAPI-path-item' + > + Path Item +
+
setHoverState((prevState) => ({ ...prevState, Summary: true }))} + onMouseLeave={() => { + return setHoverState({ ...hoverState, Summary: false }); + }} + data-testid='OpenAPI-summary' + > + Summary and description +
+
+
setHoverState((prevState) => ({ ...prevState, Operation: true }))} + onMouseLeave={() => setHoverState({ ...hoverState, Operation: false })} + data-testid='OpenAPI-operation' + > + Operation (GET, PUT, POST, etc.) +
+
setHoverState((prevState) => ({ ...prevState, Message: true }))} + onMouseLeave={() => setHoverState({ ...hoverState, Message: false })} + data-testid='OpenAPI-request' + > + Request +
+
setHoverState((prevState) => ({ ...prevState, Message: true }))} + onMouseLeave={() => setHoverState({ ...hoverState, Message: false })} + data-testid='OpenAPI-responses' + > + Responses +
+
+
+
+
+
+
+
+
+
setHoverState((prevState) => ({ ...prevState, Tags: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Tags: false }))} + data-testid='OpenAPI-tags' + > +

Tags

+
+
setHoverState((prevState) => ({ ...prevState, External: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, External: false }))} + data-testid='OpenAPI-external' + > +

External Docs

+
+
+
setHoverState((prevState) => ({ ...prevState, Components: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Components: false }))} + data-testid='OpenAPI-components' + > + Components +
+
Schemas
+
Responses
+
Parameters
+
Examples
+
Request Bodies
+
Headers
+
Security Schemes
+
Links
+
Callbacks
+
+
+
+
+
+

AsyncAPI 2.0

+
+
setHoverState((prevState) => ({ ...prevState, Info: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Info: false }))} + > + Info +
+
+
setHoverState((prevState) => ({ ...prevState, Servers: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Servers: false }))} + > + Servers (hosts + security) +
+
+
setHoverState((prevState) => ({ ...prevState, Paths: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Paths: false }))} + > + Channel +
+
setHoverState((prevState) => ({ ...prevState, PathItem: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, PathItem: false }))} + > + Channel Item +
+
+
setHoverState((prevState) => ({ ...prevState, Operation: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Operation: false }))} + > + Operation (Publish and Subscribe) +
+
setHoverState((prevState) => ({ ...prevState, Summary: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Summary: false }))} + > + Summary, description, tags, etc. +
+
+
setHoverState((prevState) => ({ ...prevState, Message: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Message: false }))} + > + Message +
Headers
+
Payload
+
+
+
+
+
+
+
+
+
+
+
+ Id (application identifier) +
+
+
+
setHoverState((prevState) => ({ ...prevState, Tags: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Tags: false }))} + > +

Tags

+
+
setHoverState((prevState) => ({ ...prevState, External: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, External: false }))} + > +

External Docs

+
+
+
setHoverState((prevState) => ({ ...prevState, Components: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Components: false }))} + > + Components +
+
Schemas
+
Messages
+
Security Schemes
+
Parameters
+
Correlation Ids
+
Operation Traits
+
Message Traits
+
Server Bindings
+
Channel Bindings
+
Operation Bindings
+
Message Bindings
+
+
+
+
+
+ ); +} diff --git a/components/OpenAPIComparisonV3.tsx b/components/OpenAPIComparisonV3.tsx new file mode 100644 index 00000000000..5fe9582ef24 --- /dev/null +++ b/components/OpenAPIComparisonV3.tsx @@ -0,0 +1,304 @@ +import React, { useState } from 'react'; + +interface HoverState { + Info: boolean; + Servers: boolean; + Paths: boolean; + PathItem: boolean; + Summary: boolean; + Operations: boolean; + OperationItem: boolean; + OperationType: boolean; + Message: boolean; + Tags: boolean; + External: boolean; + Components: boolean; +} + +interface OpenAPIComparisonV3Props { + className?: string; +} + +/** + * @description OpenAPIComparisonV3 component displays a comparison between OpenAPI 3.0 and AsyncAPI 3.0 specifications. + * @param {string} [props.className=''] - Additional CSS classes for styling + */ +export default function OpenAPIComparisonV3({ className = '' }: OpenAPIComparisonV3Props) { + const [hoverState, setHoverState] = useState({ + Info: false, + Servers: false, + Paths: false, + PathItem: true, + Summary: false, + Operations: false, + OperationItem: true, + OperationType: false, + Message: false, + Tags: false, + External: false, + Components: false + }); + + return ( +
+
+

OpenAPI 3.0

+
+
setHoverState((prevState) => ({ ...prevState, Info: true }))} + onMouseLeave={() => setHoverState({ ...hoverState, Info: false })} + > + Info +
+
+
setHoverState((prevState) => ({ ...prevState, Servers: true }))} + onMouseLeave={() => setHoverState({ ...hoverState, Servers: false })} + > + Servers +
+
Security
+
+
setHoverState((prevState) => ({ ...prevState, Paths: true }))} + onMouseLeave={() => setHoverState({ ...hoverState, Paths: false })} + > + Paths +
+
{ + return setHoverState((prevState) => ({ ...prevState, PathItem: true })); + }} + onMouseLeave={() => { + return setHoverState({ ...hoverState, PathItem: false }); + }} + > + Path Item +
+
setHoverState((prevState) => ({ ...prevState, Summary: true }))} + onMouseLeave={() => { + return setHoverState({ ...hoverState, Summary: false }); + }} + > + Summary and description +
+
+
setHoverState((prevState) => ({ ...prevState, OperationItem: true }))} + onMouseLeave={() => setHoverState({ ...hoverState, OperationItem: false })} + > + Operation +
setHoverState((prevState) => ({ ...prevState, OperationType: true }))} + onMouseLeave={() => setHoverState({ ...hoverState, OperationType: false })} + > + GET, PUT, POST, etc. +
+
setHoverState((prevState) => ({ ...prevState, Message: true }))} + onMouseLeave={() => setHoverState({ ...hoverState, Message: false })} + > + Request +
+
setHoverState((prevState) => ({ ...prevState, Message: true }))} + onMouseLeave={() => setHoverState({ ...hoverState, Message: false })} + > + Responses +
+
+
+
+
+
+
+
+
setHoverState((prevState) => ({ ...prevState, Tags: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Tags: false }))} + > +

Tags

+
+
setHoverState((prevState) => ({ ...prevState, External: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, External: false }))} + > +

External Docs

+
+
+
setHoverState((prevState) => ({ ...prevState, Components: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Components: false }))} + > + Components +
+
Definitions
+
Responses
+
Parameters
+
Response Headers
+
Security Definitions
+
Callbacks
+
Links
+
+
+
+
+
+

AsyncAPI 3.0

+
+
setHoverState((prevState) => ({ ...prevState, Info: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Info: false }))} + > + Info +
+
+
setHoverState((prevState) => ({ ...prevState, Servers: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Servers: false }))} + > + Servers (hosts + security) +
+
+
setHoverState((prevState) => ({ ...prevState, Paths: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Paths: false }))} + > + Channels +
+
setHoverState((prevState) => ({ ...prevState, PathItem: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, PathItem: false }))} + > + Channel +
+
+
setHoverState((prevState) => ({ ...prevState, Summary: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Summary: false }))} + > + Summary, description +
+
+
setHoverState((prevState) => ({ ...prevState, Message: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Message: false }))} + > + Messages +
Headers
+
Payload
+
+
+
+
+
+
+
+
setHoverState((prevState) => ({ ...prevState, Operations: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Operations: false }))} + > + Operations +
+
setHoverState((prevState) => ({ ...prevState, OperationItem: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, OperationItem: false }))} + > + Operation +
+
setHoverState((prevState) => ({ ...prevState, OperationType: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, OperationType: false }))} + > + action (send or receive) +
+
{ + return setHoverState((prevState) => ({ ...prevState, PathItem: true })); + }} + onMouseLeave={() => { + return setHoverState({ ...hoverState, PathItem: false }); + }} + > + Channel reference +
+
setHoverState((prevState) => ({ ...prevState, Message: true }))} + onMouseLeave={() => setHoverState({ ...hoverState, Message: false })} + > + Messages reference +
+
+
+
+
+
+
+ Id (application identifier) +
+
+
+
setHoverState((prevState) => ({ ...prevState, Tags: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Tags: false }))} + > +

Tags

+
+
setHoverState((prevState) => ({ ...prevState, External: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, External: false }))} + > +

External Docs

+
+
+
setHoverState((prevState) => ({ ...prevState, Components: true }))} + onMouseLeave={() => setHoverState((prevState) => ({ ...prevState, Components: false }))} + > + Components +
+
Schemas
+
Messages
+
Security Schemes
+
Parameters
+
Correlation Ids
+
Operation Traits
+
Message Traits
+
Server Bindings
+
Channel Bindings
+
Operation Bindings
+
Message Bindings
+
+
+
+
+
+ ); +} diff --git a/components/Profiles.tsx b/components/Profiles.tsx new file mode 100644 index 00000000000..aeb1f567bd5 --- /dev/null +++ b/components/Profiles.tsx @@ -0,0 +1,41 @@ +type Profile = { + name: string; + link: string; + avatar: string; +}; + +interface ProfileProps { + profiles?: Profile[]; +} + +/** + * This component displays list of profiles. + * @param {ProfileProps} props - The props for the Profiles component + * @param {Profile[]} props.profiles - List of profiles + */ +export default function Profiles({ profiles = [] }: ProfileProps) { + if (profiles.length === 0) { + return null; + } + + return ( +
+ {profiles.map((profile) => ( + + {profile.name} + {profile.name} + + ))} +
+ ); +} diff --git a/components/Remember.tsx b/components/Remember.tsx new file mode 100644 index 00000000000..2f61af39d0e --- /dev/null +++ b/components/Remember.tsx @@ -0,0 +1,28 @@ +import LightBulb from './icons/LightBulb'; + +interface RememberProps { + title?: string; + className?: string; + children: string; +} + +/** + * This component displays a remember component. + * @param {RememberProps} props - The props for the Remember component + * @param {string} props.title - The title of the remember component + * @param {string} props.className - Additional classes for the figure + * @param {string} props.children - The content of the remember component + */ +export default function Remember({ title = 'Remember', className = '', children }: RememberProps) { + return ( +
+
+ + + {title} + +
+
{children}
+
+ ); +} diff --git a/components/TOC.tsx b/components/TOC.tsx new file mode 100644 index 00000000000..89667a72468 --- /dev/null +++ b/components/TOC.tsx @@ -0,0 +1,99 @@ +import { useState } from 'react'; +import Scrollspy from 'react-scrollspy'; +import { twMerge } from 'tailwind-merge'; + +import ArrowRight from './icons/ArrowRight'; + +interface ITOCProps { + className?: string; + cssBreakingPoint?: string; + toc: { + lvl: number; + content: string; + slug: string; + }[]; + contentSelector?: string; + depth?: number; +} + +/** + * @description The table of contents + * @param {string} props.className - The class name of the component + * @param {string} props.cssBreakingPoint - The CSS breaking point + * @param {Array} props.toc - The table of contents + * @param {string} props.contentSelector - The content selector + * @param {number} props.depth - The depth of the table of contents + */ +export default function TOC({ className, cssBreakingPoint = 'xl', toc, contentSelector, depth = 2 }: ITOCProps) { + const [open, setOpen] = useState(false); + + if (!toc || !toc.length) return null; + const minLevel = toc.reduce((mLevel, item) => (!mLevel || item.lvl < mLevel ? item.lvl : mLevel), 0); + const tocItems = toc + .filter((item) => item.lvl <= minLevel + depth) + .map((item) => ({ + ...item, + content: item.content.replace(/[\s]?\{#[\w\d\-_]+\}$/, '').replace(/(<([^>]+)>)/gi, ''), + // For TOC rendering in specification files in the spec repo we have "a" tags added manually to the spec + // markdown document MDX takes these "a" tags and uses them to render the "id" for headers like + // a-namedefinitionsapplicationaapplication slugWithATag contains transformed heading name that is later used + // for scroll spy identification + slugWithATag: item.content + .replace(/[<>?!:`'."\\/=]/gi, '') + .replace(/\s/gi, '-') + .toLowerCase() + })); + + return ( +
setOpen(!open)} + > +
+
+ On this page +
+
+ +
+
+
+ item.slug)} + currentClassName='text-primary-500 font-bold' + componentTag='div' + rootEl={contentSelector} + offset={-120} + > + {tocItems.map((item, index) => ( + + {item.content.replaceAll('`', '')} + + ))} + +
+
+ ); +} diff --git a/components/Warning.tsx b/components/Warning.tsx new file mode 100644 index 00000000000..dceff8014d5 --- /dev/null +++ b/components/Warning.tsx @@ -0,0 +1,34 @@ +import IconExclamation from './icons/Exclamation'; + +interface WarningProps { + title: string; + className?: string; + description: string; +} + +/** + * This component displays a warning component. + * @param {WarningProps} props - The props for the warning component + * @param {string} props.title - The title of the warning component + * @param {string} props.className - Additional classes for the figure + * @param {string} props.description - The content of the warning component + */ +export default function Warning({ className = '', title, description }: WarningProps) { + return ( +
+
+
+ +
+
+

+ {title} +

+
+

{description}

+
+
+
+
+ ); +} diff --git a/components/campaigns/AnnouncementBanner.tsx b/components/campaigns/AnnouncementBanner.tsx new file mode 100644 index 00000000000..dc595f41573 --- /dev/null +++ b/components/campaigns/AnnouncementBanner.tsx @@ -0,0 +1,69 @@ +import React from 'react'; + +import { HeadingLevel, HeadingTypeStyle } from '@/types/typography/Heading'; +import { ParagraphTypeStyle } from '@/types/typography/Paragraph'; + +import Button from '../buttons/Button'; +import Heading from '../typography/Heading'; +import Paragraph from '../typography/Paragraph'; +import AnnouncementRemainingDays from './AnnouncementRemainingDays'; + +interface BannerProps { + title: string; + dateLocation: string; + cfaText: string; + eventName: string; + cfpDeadline: string; + link: string; + city: string; + activeBanner: boolean; + small: boolean; + className: string; +} + +/** + * @description The banner to use for Announcement + * @param {string} props.title - The title of the banner + * @param {string} props.dateLocation - The date and location of the banner + * @param {string} props.cfaText - The call for action text + * @param {string} props.eventName - The name of the event + * @param {string} props.cfpDeadline - The deadline for the call for speakers + * @param {string} props.link - The link of the banner + * @param {string} props.city - The city of the banner + * @param {Boolean} props.activeBanner - Whether the banner is active + * @param {Boolean} props.small - Whether the banner is small + * @param {string} props.className - The class name of the banner + */ +export default function Banner({ + title, + dateLocation, + cfaText, + eventName, + cfpDeadline, + link, + city, + activeBanner, + small, + className +}: BannerProps) { + return ( +
+ + {title} + + + {city} + + {dateLocation} + +
+
+
+ ); +} diff --git a/components/campaigns/AnnouncementHero.tsx b/components/campaigns/AnnouncementHero.tsx new file mode 100644 index 00000000000..1699a07440a --- /dev/null +++ b/components/campaigns/AnnouncementHero.tsx @@ -0,0 +1,98 @@ +import { useEffect, useState } from 'react'; + +import ArrowLeft from '../icons/ArrowLeft'; +import ArrowRight from '../icons/ArrowRight'; +import Container from '../layout/Container'; +import Banner from './AnnouncementBanner'; +import { banners } from './banners'; + +interface IAnnouncementHeroProps { + className?: string; + small?: boolean; +} + +/** + * @param {string} props.className - The class name of the announcement hero + * @param {Boolean} props.small - Whether the banner is small + * @param {Boolean} props.hideVideo - Whether the video should be hidden + * @description The announcement hero + */ +export default function AnnouncementHero({ className = '', small = false }: IAnnouncementHeroProps) { + const [activeIndex, setActiveIndex] = useState(0); + + const len = banners.length; + + const goToPrevious = () => { + setActiveIndex((prevIndex) => (prevIndex === 0 ? len - 1 : prevIndex - 1)); + }; + + const goToNext = () => { + setActiveIndex((prevIndex) => (prevIndex === len - 1 ? 0 : prevIndex + 1)); + }; + + const goToIndex = (index: number) => { + setActiveIndex(index); + }; + + useEffect(() => { + const interval = setInterval(() => setActiveIndex((index) => index + 1), 5000); + + return () => { + clearInterval(interval); + }; + }, [activeIndex]); + + return ( + +
+
+ +
+
+
+ {banners.map( + (banner, index) => + banner.show && ( + + ) + )} +
+
+ {banners.map((banner, index) => ( +
goToIndex(index)} + /> + ))} +
+
+
+ +
+
+ + ); +} diff --git a/components/campaigns/AnnouncementRemainingDays.tsx b/components/campaigns/AnnouncementRemainingDays.tsx new file mode 100644 index 00000000000..a462d98dc69 --- /dev/null +++ b/components/campaigns/AnnouncementRemainingDays.tsx @@ -0,0 +1,36 @@ +import moment from 'moment'; +import React from 'react'; + +interface AnnouncementRemainingDaysProps { + dateTime: string; + eventName: string; +} + +/** + * @description The announcement remaining days + * @param {string} props.dateTime - The date and time of the announcement + * @param {string} props.eventName - The name of the event + */ +export default function AnnouncementRemainingDays({ dateTime, eventName }: AnnouncementRemainingDaysProps) { + const date = moment(dateTime); + const now = moment(); + const days = date.diff(now, 'days'); + const hours = date.diff(now, 'hours'); + const minutes = date.diff(now, 'minutes'); + + let text = ''; + + if (days >= 1) { + text = `${days} ${days === 1 ? 'day' : 'days'}`; + } else if (hours > 1) { + text = 'A few hours'; + } else if (minutes > 1) { + text = 'A few minutes'; + } + + return ( + + {text} until {eventName} + + ); +} diff --git a/components/campaigns/Banner.tsx b/components/campaigns/Banner.tsx new file mode 100644 index 00000000000..65517eb10da --- /dev/null +++ b/components/campaigns/Banner.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +/** + * @description The banner to use for Announcement of AsyncAPI releases + */ +export default function Banner() { + const day = new Date().getUTCDate(); + const month = new Date().getUTCMonth(); + const year = new Date().getUTCFullYear(); + + // month=11 is December. Show only between 6-31 December. + + if (year > 2023 || month > 11 || day < 6) { + return null; + } + + return ( +
+
+
+
+

+ AsyncAPI v3 has landed! ⭐️ +

+
+ +
+
+
+ ); +} diff --git a/components/campaigns/banners.ts b/components/campaigns/banners.ts new file mode 100644 index 00000000000..e1c73fe1c5e --- /dev/null +++ b/components/campaigns/banners.ts @@ -0,0 +1,45 @@ +const cfpDeadlineIndia = '2023-11-30T06:00:00Z'; +const cfpDeadlineFrance = '2023-12-06T06:00:00Z'; + +/** + * @param {string} cfpDeadline - The deadline for the call for papers + * @returns Whether the banner should be shown + * @description Check if the current date is after the deadline + */ +function shouldShowBanner(cfpDeadline: string) { + const currentDate = new Date(); // G et the current date + const deadline = new Date(cfpDeadline); // Convert the cfpDeadline string to a Date object + + // Check if the current date is after the deadline + if (currentDate > deadline) { + return false; + } + + return true; +} + +const showBannerIndia = shouldShowBanner(cfpDeadlineIndia); +const showBannerFrance = shouldShowBanner(cfpDeadlineFrance); + +export const banners = [ + { + title: 'AsyncAPI Conf', + city: 'Bengaluru', + dateLocation: '30th of November, 2023 | Bengaluru, India', + cfaText: 'Grab Free Tickets', + eventName: "AACoT'23 Bengaluru Edition", + cfpDeadline: cfpDeadlineIndia, + link: 'https://conference.asyncapi.com/venue/bangalore', + show: showBannerIndia + }, + { + title: 'AsyncAPI Conf', + city: 'Paris', + dateLocation: '8th of December, 2023 | Paris, France', + cfaText: 'Get Free Tickets', + eventName: "AACoT'23 Paris Edition", + cfpDeadline: cfpDeadlineFrance, + link: 'https://ticket.apidays.global/event/apidays-paris-2023/8a1f3904-e2be-4c69-a880-37d2ddf1027d/cart?coupon=ASYNCAPICONF23', + show: showBannerFrance + } +]; diff --git a/components/layout/BlogLayout.tsx b/components/layout/BlogLayout.tsx new file mode 100644 index 00000000000..cb8a0f0530f --- /dev/null +++ b/components/layout/BlogLayout.tsx @@ -0,0 +1,119 @@ +import moment from 'moment'; +import ErrorPage from 'next/error'; +import HtmlHead from 'next/head'; +import { useRouter } from 'next/router'; + +import type { IPosts } from '@/types/post'; + +import BlogContext from '../../context/BlogContext'; +import AuthorAvatars from '../AuthorAvatars'; +// import AnnouncementHero from '../campaigns/AnnoucementHero'; +import Head from '../Head'; +import TOC from '../TOC'; +import Container from './Container'; + +interface IBlogLayoutProps { + post: IPosts['blog'][number]; + children: React.ReactNode; + navItems?: IPosts['blog']; +} + +/** + * @description The blog layout with the post and its content + * @param {IPosts['blog'][number]} props.post - The post + * @param {React.ReactNode} props.children - The children + */ +export default function BlogLayout({ + post, + children, + // eslint-disable-next-line unused-imports/no-unused-vars, no-unused-vars + navItems +}: IBlogLayoutProps) { + const router = useRouter(); + + if (!post) return ; + if (post.title === undefined) throw new Error('Post title is required'); + + if (!router.isFallback && !post?.slug) { + return ; + } + + return ( + + {/* */} + + +
+
+

+ {post.title} +

+
+
+ +
+
+

+ + {post.authors + .map((author, index) => + author.link ? ( + + {author.name} + + ) : ( + author.name + ) + ) + .reduce((prev, curr) => [prev, ' & ', curr] as any)} + +

+
+ + · + {post.readingTime} min read +
+
+
+
+
+ + + +
diff --git a/types/components/FigurePropsType.ts b/types/components/FigurePropsType.ts new file mode 100644 index 00000000000..6b8d8035b3d --- /dev/null +++ b/types/components/FigurePropsType.ts @@ -0,0 +1,4 @@ +export enum Float { + LEFT = 'left', + RIGHT = 'right', +};