From ce3446d32af4ab7bf733b4422c912c745d220139 Mon Sep 17 00:00:00 2001 From: Ashish Padhy <100484401+Shurtu-gal@users.noreply.github.com> Date: Thu, 28 Mar 2024 20:20:22 +0530 Subject: [PATCH] feat: migrate layout and campaigns (#2743) Co-authored-by: Akshat Nema <76521428+akshatnema@users.noreply.github.com>%0ACo-authored-by: akshatnema %0ACo-authored-by: aks-nema %0ACo-authored-by: Ansh Goyal --- components/AuthorAvatars.tsx | 4 +- components/Head.tsx | 88 +++++++ components/TOC.tsx | 99 +++++++ components/campaigns/AnnouncementBanner.tsx | 69 +++++ components/campaigns/AnnouncementHero.tsx | 98 +++++++ .../campaigns/AnnouncementRemainingDays.tsx | 36 +++ components/campaigns/Banner.tsx | 48 ++++ components/campaigns/banners.ts | 45 ++++ components/layout/BlogLayout.tsx | 119 +++++++++ components/layout/Column.tsx | 15 ++ components/layout/Container.tsx | 8 +- components/layout/GenericLayout.tsx | 52 ++++ components/layout/GenericPostLayout.tsx | 53 ++++ components/layout/GenericWideLayout.tsx | 41 +++ components/layout/Layout.tsx | 60 +++++ components/layout/Row.tsx | 11 + context/BlogContext.tsx | 5 +- context/DocsContext.tsx | 2 +- context/GenericPostContext.tsx | 5 +- package-lock.json | 246 ++++++++++-------- package.json | 2 + pages/_app.tsx | 22 +- pages/_document.tsx | 19 +- 23 files changed, 1021 insertions(+), 126 deletions(-) create mode 100644 components/Head.tsx create mode 100644 components/TOC.tsx create mode 100644 components/campaigns/AnnouncementBanner.tsx create mode 100644 components/campaigns/AnnouncementHero.tsx create mode 100644 components/campaigns/AnnouncementRemainingDays.tsx create mode 100644 components/campaigns/Banner.tsx create mode 100644 components/campaigns/banners.ts create mode 100644 components/layout/BlogLayout.tsx create mode 100644 components/layout/Column.tsx create mode 100644 components/layout/GenericLayout.tsx create mode 100644 components/layout/GenericPostLayout.tsx create mode 100644 components/layout/GenericWideLayout.tsx create mode 100644 components/layout/Layout.tsx create mode 100644 components/layout/Row.tsx 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/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/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/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 +
+
+
+
+
+ + + +