diff --git a/apps/site/components/Common/NodejsLogo/index.tsx b/apps/site/components/Common/NodejsLogo/index.tsx index 7ca383e10c505..b6b43f48d505a 100644 --- a/apps/site/components/Common/NodejsLogo/index.tsx +++ b/apps/site/components/Common/NodejsLogo/index.tsx @@ -1,16 +1,16 @@ -import type { FC } from 'react'; +import type { ComponentProps, FC } from 'react'; import Nodejs from '@/components/Icons/Logos/Nodejs'; import type { LogoVariant } from '@/types'; import style from './index.module.css'; -type NodejsLogoProps = { +type NodejsLogoProps = ComponentProps & { variant?: LogoVariant; }; -const NodejsLogo: FC = ({ variant = 'default' }) => ( - +const NodejsLogo: FC = ({ variant = 'default', ...props }) => ( + ); export default NodejsLogo; diff --git a/apps/site/components/Common/Supporters/SupporterIcon/index.module.css b/apps/site/components/Common/Supporters/SupporterIcon/index.module.css new file mode 100644 index 0000000000000..679e960e0ef38 --- /dev/null +++ b/apps/site/components/Common/Supporters/SupporterIcon/index.module.css @@ -0,0 +1,7 @@ +.supporterIcon { + @apply h-12 + w-12 + rounded-lg + bg-neutral-100 + p-1; +} diff --git a/apps/site/components/Common/Supporters/SupporterIcon/index.tsx b/apps/site/components/Common/Supporters/SupporterIcon/index.tsx new file mode 100644 index 0000000000000..b27246bb2e792 --- /dev/null +++ b/apps/site/components/Common/Supporters/SupporterIcon/index.tsx @@ -0,0 +1,24 @@ +import Link from 'next/link'; +import { cloneElement, type ComponentProps, type FC } from 'react'; + +import Skeleton from '@/components/Common/Skeleton'; +import type { Supporter } from '@/types'; + +import style from './index.module.css'; + +type SupporterIconProps = Supporter & ComponentProps; + +const SupporterIcon: FC = ({ + name, + href, + icon, + loading = false, +}) => ( + + + {cloneElement(icon, { className: style.supporterIcon })} + + +); + +export default SupporterIcon; diff --git a/apps/site/components/Common/Supporters/SupporterIconList/index.module.css b/apps/site/components/Common/Supporters/SupporterIconList/index.module.css new file mode 100644 index 0000000000000..a281d91ac9363 --- /dev/null +++ b/apps/site/components/Common/Supporters/SupporterIconList/index.module.css @@ -0,0 +1,8 @@ +.supporterIconList { + @apply flex + flex-row + flex-wrap + items-center + justify-center + gap-4; +} diff --git a/apps/site/components/Common/Supporters/SupporterIconList/index.tsx b/apps/site/components/Common/Supporters/SupporterIconList/index.tsx new file mode 100644 index 0000000000000..9451707df2b97 --- /dev/null +++ b/apps/site/components/Common/Supporters/SupporterIconList/index.tsx @@ -0,0 +1,67 @@ +'use client'; + +import { useEffect, useRef, useState, type FC } from 'react'; + +import SupporterIcon from '@/components/Common/Supporters/SupporterIcon'; +import { randomSupporterList } from '@/components/Common/Supporters/utils'; +import { DEFAULT_SUPPORTERS_LIST } from '@/next.supporters.constants'; +import type { Supporter } from '@/types'; + +import style from './index.module.css'; + +type SupporterIconListProps = { + supporters: Array; + maxLength?: number; +}; + +const SupporterIconList: FC = ({ + supporters, + maxLength = 4, +}) => { + const initialRenderer = useRef(true); + + const [seedList, setSeedList] = useState>( + DEFAULT_SUPPORTERS_LIST + ); + + useEffect(() => { + // We intentionally render the initial default "mock" list of sponsors + // to have the Skeletons loading, and then we render the actual list + // after an enough amount of time has passed to give a proper sense of Animation + // We do this client-side effect, to ensure that a random-amount of sponsors is renderered + // on every page load. Since our page is natively static, we need to ensure that + // on the client-side we have a random amount of sponsors rendered. + // Although whilst we are deployed on Vercel or other environment that supports ISR + // (Incremental Static Generation) whose would invalidate the cache every 5 minutes + // We want to ensure that this feature is compatible on a full-static environment + const renderSponsorsAnimation = setTimeout(() => { + initialRenderer.current = false; + + setSeedList( + randomSupporterList( + supporters, + Math.max(supporters.length, maxLength * 2) + ) + ); + }, 1000); + + return () => clearTimeout(renderSponsorsAnimation); + // We only want this to run once on initial render + // We don't really care if the props change as realistically they shouldn't ever + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+ {seedList.slice(0, maxLength).map((supporter, index) => ( + + ))} +
+ ); +}; + +export default SupporterIconList; diff --git a/apps/site/components/Common/Supporters/SupporterLogo/index.tsx b/apps/site/components/Common/Supporters/SupporterLogo/index.tsx new file mode 100644 index 0000000000000..04ea0013b8216 --- /dev/null +++ b/apps/site/components/Common/Supporters/SupporterLogo/index.tsx @@ -0,0 +1,28 @@ +import classNames from 'classnames'; +import { cloneElement, type FC } from 'react'; + +import Link from '@/components/Link'; +import type { Supporter } from '@/types'; + +const SupporterLogoSizeByThreshold: Record = { + 0.1: 'h-8 w-8', + 0.2: 'h-10 w-10', + 0.3: 'h-12 w-12', + 0.4: 'h-14 w-14', + 0.5: 'h-16 w-16', + 0.6: 'h-18 w-18', + 0.7: 'h-20 w-20', + 0.8: 'h-22 w-22', + 0.9: 'h-24 w-24', + 1: 'h-24 w-24', +}; + +const SupporterLogo: FC = ({ href, logo, name, threshold }) => ( + + {cloneElement(logo, { + className: classNames('p-1', SupporterLogoSizeByThreshold[threshold]), + })} + +); + +export default SupporterLogo; diff --git a/apps/site/components/Common/Supporters/SupporterLogoList/index.module.css b/apps/site/components/Common/Supporters/SupporterLogoList/index.module.css new file mode 100644 index 0000000000000..edc44ad26c355 --- /dev/null +++ b/apps/site/components/Common/Supporters/SupporterLogoList/index.module.css @@ -0,0 +1,22 @@ +.supporterLogoList { + @apply flex + flex-col + gap-2; +} + +.supporterTier { + @apply flex + flex-wrap + items-center + justify-center + gap-4 + last-of-type:mb-0 + md:gap-6; +} + +.supporterLogo { + @apply flex + items-center + justify-center + p-2; +} diff --git a/apps/site/components/Common/Supporters/SupporterLogoList/index.tsx b/apps/site/components/Common/Supporters/SupporterLogoList/index.tsx new file mode 100644 index 0000000000000..2795b7bc531bc --- /dev/null +++ b/apps/site/components/Common/Supporters/SupporterLogoList/index.tsx @@ -0,0 +1,51 @@ +'use client'; + +import type { FC } from 'react'; +import { useMemo } from 'react'; + +import SupporterLogo from '@/components/Common/Supporters/SupporterLogo'; +import type { Supporter } from '@/types'; + +import styles from './index.module.css'; + +type SupporterLogoListProps = { + supporters: Array; +}; + +const SupporterLogoList: FC = ({ supporters }) => { + const tiers = useMemo(() => { + const sortedSupporters = [...supporters].sort( + (a, b) => b.threshold - a.threshold + ); + + return sortedSupporters.reduce>>( + (acc, supporter) => ({ + ...acc, + [supporter.threshold]: [...(acc[supporter.threshold] ?? []), supporter], + }), + {} + ); + }, [supporters]); + + return ( +
+ {Object.keys(tiers).map((threshold, index) => ( +
+ {tiers[threshold].map((supporter, idx) => ( +
+ +
+ ))} +
+ ))} +
+ ); +}; + +export default SupporterLogoList; diff --git a/apps/site/components/Common/Supporters/utils.ts b/apps/site/components/Common/Supporters/utils.ts new file mode 100644 index 0000000000000..0e0fee9c89340 --- /dev/null +++ b/apps/site/components/Common/Supporters/utils.ts @@ -0,0 +1,10 @@ +import type { Supporter } from '@/types'; + +export const randomSupporterList = ( + supporters: Array, + pick: number +) => + supporters + .sort(() => 0.5 - Math.random()) + .slice(0, pick) + .sort((a, b) => b.threshold - a.threshold); diff --git a/apps/site/components/Containers/Footer/index.tsx b/apps/site/components/Containers/Footer/index.tsx index e2d2411db1098..f0ec9c5eba6c6 100644 --- a/apps/site/components/Containers/Footer/index.tsx +++ b/apps/site/components/Containers/Footer/index.tsx @@ -8,7 +8,7 @@ import LinkedIn from '@/components/Icons/Social/LinkedIn'; import Mastodon from '@/components/Icons/Social/Mastodon'; import Slack from '@/components/Icons/Social/Slack'; import Twitter from '@/components/Icons/Social/Twitter'; -import { siteNavigation } from '@/next.json.mjs'; +import type { FooterConfig, SocialConfig } from '@/types'; import styles from './index.module.css'; @@ -21,15 +21,20 @@ const footerSocialIcons: Record>> = { bluesky: Bluesky, }; -const Footer: FC = () => { +type FooterProps = { + socialLinks: Array; + footerLinks: Array; +}; + +const Footer: FC = ({ socialLinks, footerLinks }) => { const t = useTranslations(); - const openJSlink = siteNavigation.footerLinks.at(-1)!; + const openJSlink = footerLinks.at(-1)!; return (