From 2fd21ad2058c4c17ed4a923ae2b66bd66925422d Mon Sep 17 00:00:00 2001 From: devilkiller-ag Date: Sat, 30 Mar 2024 13:39:18 +0530 Subject: [PATCH 1/9] migrated newsletter subscription form --- components/NewsletterSubscribe.tsx | 161 +++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 components/NewsletterSubscribe.tsx diff --git a/components/NewsletterSubscribe.tsx b/components/NewsletterSubscribe.tsx new file mode 100644 index 00000000000..875a17a657e --- /dev/null +++ b/components/NewsletterSubscribe.tsx @@ -0,0 +1,161 @@ +import { useState } from 'react'; + +import { ButtonType } from '@/types/components/buttons/ButtonPropsType'; +import { HeadingLevel, HeadingTypeStyle } from '@/types/typography/Heading'; + +import { useTranslation } from '../utils/i18n'; +import Button from './buttons/Button'; +import Loader from './Loader'; +import Heading from './typography/Heading'; +import Paragraph from './typography/Paragraph'; +import TextLink from './typography/TextLink'; + +enum FormStatus { + NORMAL = 'normal', + LOADING = 'loading', + SUCCESS = 'success', + ERROR = 'error' +} + +interface NewsletterSubscribeProps { + className: string; + dark: boolean; + title: string; + subtitle: string; + type: string; +} + +/** + * @description This component displays Newsletter Subscribe component. + * + * @param {NewsletterSubscribeProps} props - The props for the Newsletter Subscribe component. + * @param {string} props.className - CSS class for styling the card. + * @param {boolean} props.dark - If true, the theme of the component will be dark. + * @param {string} props.title - The title of the Subscribe card. + * @param {string} props.subtitle - The subtitle of the Subscribe card. + * @param {string} props.type - The type of subscription. + */ +export default function NewsletterSubscribe({ + className = 'p-8 text-center', + dark = false, + title = 'Subscribe to our newsletter to receive news about AsyncAPI.', + subtitle = 'We respect your inbox. No spam, promise ✌️', + type = 'Newsletter' +}: NewsletterSubscribeProps) { + const [email, setEmail] = useState(''); + const [name, setName] = useState(''); + const [status, setStatus] = useState(FormStatus.NORMAL); + + const { t } = useTranslation('common'); + + const headTextColor = dark ? 'text-white' : ''; + const paragraphTextColor = dark ? 'text-gray-300' : ''; + + const setFormStatus = (formResponse: FormStatus) => { + setStatus(formResponse); + setTimeout(() => { + setStatus(FormStatus.NORMAL); + }, 10000); + }; + + const handleSubmit = (event: React.FormEvent) => { + setStatus(FormStatus.LOADING); + event.preventDefault(); + const data = { + name, + email, + interest: type + }; + + fetch('/.netlify/functions/newsletter_subscription', { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }) + .then((res) => { + if (res.status === 200) { + setFormStatus(FormStatus.SUCCESS); + } else { + setFormStatus(FormStatus.ERROR); + } + + return res.json(); + }) + // eslint-disable-next-line @typescript-eslint/no-shadow, no-console + .then((data) => console.log(data)); + }; + + if (status === FormStatus.SUCCESS) { + return ( +
+ + {t('newsletterCTA.successTitle')} + + + {t('newsletterCTA.subtitle')} + +
+ ); + } + + if (status === FormStatus.ERROR) { + return ( +
+ + {t('newsletterCTA.errorTitle')} + + + {t('newsletterCTA.errorSubtitle')}{' '} + + {t('newsletterCTA.errorLinkText')} + + +
+ ); + } + + return ( +
+ + {title} + + + {subtitle} + + {status === 'loading' ? ( + + ) : ( +
+ setName(e.target.value)} + className='form-input block w-full rounded-md sm:text-sm sm:leading-5 md:mt-0 md:flex-1' + required + data-testid='NewsletterSubscribe-text-input' + /> + setEmail(e.target.value)} + className='form-input mt-2 block w-full rounded-md sm:text-sm sm:leading-5 md:mt-0 md:flex-1' + required + data-testid='NewsletterSubscribe-email-input' + /> +
+ ); +} From 5127b32e3ba2632e55c24a5dd6766d9d69e6c79b Mon Sep 17 00:00:00 2001 From: devilkiller-ag Date: Sat, 30 Mar 2024 14:11:33 +0530 Subject: [PATCH 2/9] migrated testimonial --- components/NewsletterSubscribe.tsx | 10 +++--- components/Testimonial.tsx | 55 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 components/Testimonial.tsx diff --git a/components/NewsletterSubscribe.tsx b/components/NewsletterSubscribe.tsx index 875a17a657e..669a848fb49 100644 --- a/components/NewsletterSubscribe.tsx +++ b/components/NewsletterSubscribe.tsx @@ -18,11 +18,11 @@ enum FormStatus { } interface NewsletterSubscribeProps { - className: string; - dark: boolean; - title: string; - subtitle: string; - type: string; + className?: string; + dark?: boolean; + title?: string; + subtitle?: string; + type?: string; } /** diff --git a/components/Testimonial.tsx b/components/Testimonial.tsx new file mode 100644 index 00000000000..9606066f98d --- /dev/null +++ b/components/Testimonial.tsx @@ -0,0 +1,55 @@ +import Paragraph from './typography/Paragraph'; + +interface TestimonialProps { + className?: string; + text: string; + authorName: string; + authorDescription: string; + authorAvatar: string; +} + +/** + * @description This component displays Testimonial component. + * + * @param {TestimonialProps} props - The props for the Testimonial component. + * @param {string} props.className - Additional CSS class for styling the card. + * @param {string} props.text - The testimonial from the author. + * @param {string} props.authorName - The name of the author. + * @param {string} props.authorDescription - The description of the author. + * @param {string} props.authorAvatar - The path to avatar of the author. + */ +export default function Testimonial({ + className = '', + text, + authorName, + authorDescription, + authorAvatar +}: TestimonialProps) { + return ( +
  • +
    +
    + + + + {text} +
    +
    +
    +
    + {authorName} +
    +
    +

    {authorName}

    +

    {authorDescription}

    +
    +
    +
    +
    +
  • + ); +} From 14aeaf926e71a8f2a9f7a8452f579a4e0c220459 Mon Sep 17 00:00:00 2001 From: devilkiller-ag Date: Sat, 30 Mar 2024 17:33:35 +0530 Subject: [PATCH 3/9] migrated CaseTOC --- components/CaseTOC.tsx | 197 +++++++++++++++++++++ components/helpers/useHeadingsObserver.tsx | 36 ++++ 2 files changed, 233 insertions(+) create mode 100644 components/CaseTOC.tsx create mode 100644 components/helpers/useHeadingsObserver.tsx diff --git a/components/CaseTOC.tsx b/components/CaseTOC.tsx new file mode 100644 index 00000000000..3f73134251b --- /dev/null +++ b/components/CaseTOC.tsx @@ -0,0 +1,197 @@ +import { useMemo, useState } from 'react'; +import { twMerge } from 'tailwind-merge'; + +import { useHeadingsObserver } from './helpers/useHeadingsObserver'; +import ArrowRight from './icons/ArrowRight'; + +interface TocItem { + lvl: number; + content: string; + slug: string; + children?: TocItem[]; +} + +interface TOCItemProps { + item: TocItem; + index: number; + currSelected: string; + closeMenu: () => void; +} + +interface CaseTOCProps { + className: string; + cssBreakingPoint?: 'xl' | 'lg'; + toc: any[]; +} + +/** + * @description Checks if the item is active. + * + * @param {TocItem} item - The TOC item to check. + * @param {string} currSelected - The currently selected TOC item. + * @returns {boolean} - True if the item is active, otherwise false. + */ +const checkIfActive = (item: TocItem, currSelected: string): boolean => { + return item.slug === currSelected || item.children?.some((child) => checkIfActive(child, currSelected)) || false; +}; + +/** + * @description Converts content to TOC items. + * + * @param {any[]} content - The content to convert to TOC items. + * @param {number} level - The level of the TOC item. + * @returns {TocItem[]} - The array of TOC items. + */ +const convertContentToTocItems = (content: any[], level: number = 1): TocItem[] => { + const tocItems = []; + + for (const section of content) { + const item = { + lvl: level, + content: section.title, + slug: section.title + .replace(/<|>|"|\\|\/|=/gi, '') + .replace(/\s/gi, '-') + .toLowerCase() + }; + + if (section.children && section.children.length > 0) { + const children = convertContentToTocItems(section.children, level + 1); + + (item as TocItem).children = children; + } + + tocItems.push(item); + } + + return tocItems; +}; + +/** + * @description Component representing an item in the table of contents (TOC). + * + * @param {TOCItemProps} props - The props for TOCItem. + * @param {TocItem} props.item - The TOC item. + * @param {number} props.index - The index of the TOC item. + * @param {string} props.currSelected - The currently selected TOC item. + * @param {Function} props.closeMenu - A function to close the menu. + */ +function TOCItem({ item, index, currSelected, closeMenu }: TOCItemProps) { + const [open, setOpen] = useState(false); + const handleClick = () => { + closeMenu(); + setOpen(false); + }; + const active = useMemo(() => checkIfActive(item, currSelected), [item, currSelected]); + + return ( + <> + + {item.children && item.children.length > 0 && ( +
      + {item.children.map((child_item, child_index) => ( + + ))} +
    + )} + + ); +} + +/** + * @description Component representing a table of contents (TOC) for a case. + * + * @param {CaseTOCProps} props - The props for CaseTOC. + * @param {string} props.className - The CSS class name for the component. + * @param {("xl"|"lg")} [props.cssBreakingPoint="xl"] - The CSS breaking point for responsiveness. + * @param {any[]} props.toc - The table of contents data. + */ +export default function CaseTOC({ className, cssBreakingPoint = 'xl', toc }: CaseTOCProps) { + const { currActive: selected } = useHeadingsObserver(); + const [open, setOpen] = useState(false); + const tocItems = useMemo(() => convertContentToTocItems(toc), [toc]); + + if (!toc || !toc.length) return null; + + return ( +
    +
    +
    + On this page +
    +
    setOpen(!open)} + > + +
    +
    +
    +
      + {tocItems.map((item, index) => ( + setOpen(false)} + currSelected={selected || ''} + /> + ))} +
    +
    +
    + ); +} diff --git a/components/helpers/useHeadingsObserver.tsx b/components/helpers/useHeadingsObserver.tsx new file mode 100644 index 00000000000..0a340d21633 --- /dev/null +++ b/components/helpers/useHeadingsObserver.tsx @@ -0,0 +1,36 @@ +import { useEffect, useRef, useState } from 'react'; + +/** + * @description Custom hook to observe headings and set the current active heading + * @example const { currActive } = useHeadingsObserver(); + * @returns {object} currActive - current active heading + */ +export function useHeadingsObserver() { + const observer = useRef(null); + const headingsRef = useRef | []>([]); + const [currActive, setCurrActive] = useState(null); + + useEffect(() => { + const callback = (entries: IntersectionObserverEntry[]) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setCurrActive(entry.target.id); + } + }); + }; + + // The heading in from top 20% of the viewport to top 30% of the viewport will be considered as active + observer.current = new IntersectionObserver(callback, { + rootMargin: '-20% 0px -70% 0px' + }); + + headingsRef.current = document.querySelectorAll('h2, h3'); + headingsRef.current.forEach((heading) => { + observer.current?.observe(heading); + }); + + return () => observer.current?.disconnect(); + }, []); + + return { currActive }; +} From a6a4b3d6aae411a8cce2fb01d645b897e8e7cddb Mon Sep 17 00:00:00 2001 From: devilkiller-ag Date: Thu, 4 Apr 2024 19:41:42 +0530 Subject: [PATCH 4/9] migrated Inline Help and it's dependency --- components/InlineHelp.tsx | 47 ++++++++++++++++++++++++++++++++ components/helpers/click-away.ts | 36 ++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 components/InlineHelp.tsx create mode 100644 components/helpers/click-away.ts diff --git a/components/InlineHelp.tsx b/components/InlineHelp.tsx new file mode 100644 index 00000000000..ec8d039da2a --- /dev/null +++ b/components/InlineHelp.tsx @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; + +import { registerClickAway } from './helpers/click-away'; +import QuestionMark from './icons/QuestionMark'; + +interface InlineHelpProps { + className?: string; + text: string; +} + +/** + * @description This component displays Inline Help about outcome. solution, and implementation in the Roadmap page. + * @param {InlineHelpProps} props - Props for the InlineHelp component. + * @param {string} props.className - Additional CSS classes for styling. + * @param {string} props.text - The text to display in the inline help. + */ +export default function InlineHelp({ className = 'lg:relative inline-block', text }: InlineHelpProps) { + const [isHelpVisible, setIsHelpVisible] = useState(false); + + useEffect(() => { + if (isHelpVisible) { + registerClickAway(() => { + setIsHelpVisible(false); + }); + } + }, [isHelpVisible]); + + return ( +
    + {isHelpVisible && ( +
    + {text} +
    + )} + setIsHelpVisible(!isHelpVisible)} + onMouseEnter={() => setIsHelpVisible(true)} + onMouseLeave={() => setIsHelpVisible(false)} + data-testid='InlineHelp-icon' + /> +
    + ); +} diff --git a/components/helpers/click-away.ts b/components/helpers/click-away.ts new file mode 100644 index 00000000000..39df9b9d68b --- /dev/null +++ b/components/helpers/click-away.ts @@ -0,0 +1,36 @@ +/** + * @description Registers a callback function to be invoked when a click occurs outside of specified elements. + * @param {function} callback - The callback function to be invoked when a click occurs outside of specified elements. + * It takes a MouseEvent parameter and returns void. + */ +export function registerClickAway(callback: (event: MouseEvent) => void) { + /** + * @description Handles the click event and invokes the callback function if the click occurs outside of specified elements. + * @param {MouseEvent} event - The MouseEvent object representing the click event. + */ + function unregisterClickAway(event: MouseEvent) { + document.removeEventListener('click', unregisterClickAway); + + document.querySelectorAll('iframe').forEach((iframe) => { + const src = iframe.attributes.getNamedItem('src'); + + if (src && src.value.startsWith('/') && !src.value.startsWith('//')) { + iframe.contentWindow?.document.removeEventListener('click', unregisterClickAway); + } + }); + + callback(event); + } + + document.removeEventListener('click', unregisterClickAway); + document.addEventListener('click', unregisterClickAway); + + document.querySelectorAll('iframe').forEach((iframe) => { + const src = iframe.attributes.getNamedItem('src'); + + if (src && src.value.startsWith('/') && !src.value.startsWith('//')) { + iframe.contentWindow?.document.removeEventListener('click', unregisterClickAway); + iframe.contentWindow?.document.addEventListener('click', unregisterClickAway); + } + }); +} From 5d47a4bedb3b6b9dcd023f020f7c95420d031c21 Mon Sep 17 00:00:00 2001 From: devilkiller-ag Date: Thu, 4 Apr 2024 21:18:37 +0530 Subject: [PATCH 5/9] migrated GeneratorInstallation --- components/GeneratorInstallation.tsx | 99 ++++++++++++++++++++++++++++ components/editor/CodeBlock.tsx | 4 +- 2 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 components/GeneratorInstallation.tsx diff --git a/components/GeneratorInstallation.tsx b/components/GeneratorInstallation.tsx new file mode 100644 index 00000000000..d280a2199e4 --- /dev/null +++ b/components/GeneratorInstallation.tsx @@ -0,0 +1,99 @@ +import { useState } from 'react'; + +import { ParagraphTypeStyle } from '@/types/typography/Paragraph'; + +import generatorflagList from '../config/generator-flags.json'; +import generatorTemplates from '../config/generator-templates.json'; +import CodeBlock from './editor/CodeBlock'; +import Select from './form/Select'; +import Paragraph from './typography/Paragraph'; + +interface GeneratorFlagData { + flag: string; + specPath: string; +} + +interface GeneratorFlags { + [key: string]: GeneratorFlagData; +} + +/** + * @description This component displays generator installation options. + */ +export default function GeneratorInstallation() { + const [template, setTemplate] = useState('@asyncapi/html-template'); + // By default we will have output folder flag so its set here. + const [params, setParams] = useState('-o example'); + const [specPath, setSpecPath] = useState('https://bit.ly/asyncapi'); + + const generatorflags = generatorflagList as GeneratorFlags; + + /** + * @description Handles the change event when selecting a generator template. + * @param {string} templateName - The name of the selected template. + */ + function onChangeTemplate(templateName: string) { + setTemplate(templateName); + if (templateName && generatorflags[templateName]) { + const templateBasedJSON = generatorflags[templateName]; + + // options are generated from generator-templates.json + // and flags are fetched from generator-flags.json, + // so it is mandatory to have check in case if any misses the option in future + + if (templateBasedJSON) { + setParams(templateBasedJSON.flag); + setSpecPath(templateBasedJSON.specPath); + } + } + } + + /** + * @description Generates the npm install command. + * @returns {string} The npm install command. + */ + function getNpmCode(): string { + return `npm install -g @asyncapi/cli +asyncapi generate fromTemplate ${specPath} ${template} ${params}`; + } + + /** + * Generates the Docker command. + * @returns {string} The Docker command. + */ + function getDockerCode(): string { + return `docker run --rm -it -v \${PWD}/example:/app/example -v \${PWD}/output:/app/output \\ +asyncapi/cli generate fromTemplate ${specPath} ${template} ${params}`; + } + + return ( +
    +
    + + Select a Generator template: + +