Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/site/components/Common/Partners/PartnerIcon/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@reference "../../../../styles/index.css";

.partnerIcon {
@apply h-9
w-auto
min-w-9
p-2;

svg {
@apply !h-4
!w-auto;
}
}
34 changes: 34 additions & 0 deletions apps/site/components/Common/Partners/PartnerIcon/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Skeleton from '@node-core/ui-components/Common/Skeleton';
import Tooltip from '@node-core/ui-components/Common/Tooltip';
import type { ComponentProps, FC } from 'react';
import { cloneElement } from 'react';

import Button from '#site/components/Common/Button';
import type { Partners } from '#site/types';

import style from './index.module.css';

type PartnersIconProps = Partners & ComponentProps<typeof Skeleton>;

const PartnersIcon: FC<PartnersIconProps> = ({ name, href, logo, loading }) => (
<Skeleton loading={loading} className="size-9 p-2">
<Tooltip
content={
<div className="p-2 text-neutral-900 dark:text-neutral-200">{name}</div>
}
>
<Button
kind="secondary"
href={`${href}/?utm_source=nodejs-website&utm_medium=Link`}
className={style.partnerIcon}
>
{cloneElement(logo, {
width: '100%',
height: '16px',
})}
</Button>
</Tooltip>
</Skeleton>
);

export default PartnersIcon;
19 changes: 19 additions & 0 deletions apps/site/components/Common/Partners/PartnerLogo/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@reference "../../../../styles/index.css";

.partnerIcon {
@apply flex
h-28
max-h-28
w-auto
min-w-12
items-center
justify-center
rounded-lg
p-6
sm:p-10;

svg {
@apply !h-12
!w-auto;
}
}
27 changes: 27 additions & 0 deletions apps/site/components/Common/Partners/PartnerLogo/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Skeleton from '@node-core/ui-components/Common/Skeleton';
import type { ComponentProps, FC } from 'react';
import { cloneElement } from 'react';

import Button from '#site/components/Common/Button';
import type { Partners } from '#site/types';

import style from './index.module.css';

type PartnersLogoProps = Partners & ComponentProps<typeof Skeleton>;

const PartnersLogo: FC<PartnersLogoProps> = ({ href, logo, loading }) => (
<Skeleton loading={loading} className="h-28 w-full p-2">
<Button
kind="secondary"
href={`${href}/?utm_source=nodejs-website&utm_medium=Link`}
className={style.partnerIcon}
>
{cloneElement(logo, {
width: '100%',
height: '16px',
})}
</Button>
</Skeleton>
);

export default PartnersLogo;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@reference "../../../../styles/index.css";

.partnersIconList {
@apply flex
flex-row
flex-wrap
items-center
gap-2;
}
40 changes: 40 additions & 0 deletions apps/site/components/Common/Partners/PartnersIconList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client';

import type { FC } from 'react';

import usePartnersList from '#site/hooks/react-client/usePartnersList';
import { ICON_PARTNERS } from '#site/next.partners.constants';
import type { PartnerCategory } from '#site/types';

import PartnerIcon from '../PartnerIcon';
import style from './index.module.css';

type PartnersIconListProps = {
maxLength?: number;
categories?: PartnerCategory;
};

const PartnersIconList: FC<PartnersIconListProps> = ({
maxLength = 6,
categories,
}) => {
const { seedList, initialRenderer } = usePartnersList({
logos: ICON_PARTNERS,
maxLength,
categories,
});

return (
<div className={style.partnersIconList}>
{seedList.map((partner, index) => (
<PartnerIcon
{...partner}
key={index}
loading={initialRenderer.current}
/>
))}
</div>
);
};

export default PartnersIconList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@reference "../../../../styles/index.css";

.partnersLogoList {
@apply grid
w-full
grid-cols-[repeat(auto-fill,minmax(240px,1fr))]
gap-4;
}
43 changes: 43 additions & 0 deletions apps/site/components/Common/Partners/PartnersLogoList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use client';

import type { FC } from 'react';

import usePartnersList from '#site/hooks/react-client/usePartnersList';
import { LOGO_PARTNERS } from '#site/next.partners.constants';
import type { PartnerCategory } from '#site/types';

import PartnerLogo from '../PartnerLogo';
import style from './index.module.css';

type PartnersLogoListProps = {
maxLength?: number;
categories?: PartnerCategory;
sort?: 'name' | 'weight';
};

const PartnersLogoList: FC<PartnersLogoListProps> = ({
maxLength = 3,
sort = 'weight',
categories,
}) => {
const { seedList, initialRenderer } = usePartnersList({
logos: LOGO_PARTNERS,
maxLength,
sort,
categories,
});

return (
<div className={style.partnersLogoList}>
{seedList.map((partner, index) => (
<PartnerLogo
{...partner}
key={index}
loading={initialRenderer.current}
/>
))}
</div>
);
};

export default PartnersLogoList;
23 changes: 23 additions & 0 deletions apps/site/components/Common/Partners/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { RandomPartnerListConfig, Partners } from '#site/types';
import { shuffle } from '#site/util/array';

async function randomPartnerList(
partners: Array<Partners>,
config: RandomPartnerListConfig
): Promise<Array<Partners>> {
const { pick = 4, dateSeed = 5, category } = config;

// Generate a deterministic seed based on current time that changes every X minutes
const seed = Math.floor(Date.now() / (dateSeed * 60 * 1000));

// Filter by category if provided
const filtered = category
? partners.filter(p => p.categories.includes(category))
: partners;

const shuffled = await shuffle(filtered, seed);

return shuffled.slice(0, pick ?? filtered.length);
}

export { randomPartnerList };
20 changes: 20 additions & 0 deletions apps/site/components/Common/Supporters/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client';

import Avatar from '@node-core/ui-components/Common/AvatarGroup/Avatar';
import type { FC } from 'react';

import type { Supporters } from '#site/types';

type SupportersListProps = {
supporters: Array<Supporters>;
};

const SupportersList: FC<SupportersListProps> = ({ supporters }) => (
<div className="flex max-w-full flex-wrap items-center justify-center gap-1">
{supporters.map(({ name, image }, i) => (
<Avatar nickname={name} image={image} key={`${name}-${i}`} />
))}
</div>
);

export default SupportersList;
19 changes: 19 additions & 0 deletions apps/site/components/withSupporters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use server';

import type { FC, PropsWithChildren } from 'react';

import SupportersList from './Common/Supporters';

import provideSupporters from '#site/next-data/providers/supportersData';

const WithSupporters: FC<PropsWithChildren> = () => {
const supporters = provideSupporters();

return (
<div className="flex max-w-full flex-wrap items-center gap-1">
<SupportersList supporters={supporters} />
</div>
);
};

export default WithSupporters;
61 changes: 61 additions & 0 deletions apps/site/hooks/react-client/usePartnersList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client';

import { useEffect, useRef, useState } from 'react';

import { randomPartnerList } from '#site/components/Common/Partners/utils';
import type { PartnerCategory, Partners } from '#site/types/partners';

const usePartnersList = ({
logos,
maxLength,
sort,
categories,
}: {
logos: Array<Partners>;
maxLength: number;
sort?: 'name' | 'weight';
categories?: PartnerCategory;
}) => {
const initialRenderer = useRef(true);

const [seedList, setSeedList] = useState<Array<Partners>>(() => {
const filteredLogos = logos.filter(
partner => !categories || partner.categories.includes(categories)
);

return filteredLogos.slice(0, maxLength || filteredLogos.length);
});

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(async () => {
initialRenderer.current = false;

setSeedList(
await randomPartnerList(logos, {
pick: maxLength,
dateSeed: 1,
category: categories,
sort,
})
);
}, 0);

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, initialRenderer };
};

export default usePartnersList;
9 changes: 9 additions & 0 deletions apps/site/mdx/components.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import MDXCodeTabs from '@node-core/ui-components/MDX/CodeTabs';

import Button from '#site/components/Common/Button';
import LinkWithArrow from '#site/components/Common/LinkWithArrow';
import PartnersIconList from '#site/components/Common/Partners/PartnersIconList';
import PartnersLogoList from '#site/components/Common/Partners/PartnersLogoList';
import DownloadButton from '#site/components/Downloads/DownloadButton';
import DownloadsTable from '#site/components/Downloads/DownloadsTable';
import BlogPostLink from '#site/components/Downloads/Release/BlogPostLink';
Expand Down Expand Up @@ -33,6 +35,7 @@ import WithDownloadArchive from '#site/components/withDownloadArchive';
import WithNodeRelease from '#site/components/withNodeRelease';
import WithReleaseAlertBox from '#site/components/withReleaseAlertBox';
import WithReleaseSelect from '#site/components/withReleaseSelect';
import WithSupporters from '#site/components/withSupporters';
import { ReleaseProvider } from '#site/providers/releaseProvider';

/**
Expand Down Expand Up @@ -67,6 +70,10 @@ export default {
ChangelogLink,
DownloadLink: ReleaseDownloadLink,
},
// Shows a list of Node.js Partners with Icons
PartnersIconList,
// Shows a list of Node.js Partners with Logos
PartnersLogoList,
// HOC for providing the Download Archive Page properties
WithDownloadArchive,
DownloadsTable,
Expand All @@ -75,6 +82,8 @@ export default {
WithReleaseAlertBox,
WithBanner,
WithBadgeGroup,
// HOC for providing Backers Data
WithSupporters,
BadgeGroup,
ReleaseOverview,
MinorReleasesTable,
Expand Down
4 changes: 4 additions & 0 deletions apps/site/navigation.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@
"link": "/about/governance",
"label": "components.navigation.about.links.governance"
},
"partners": {
"link": "/about/partners",
"label": "components.navigation.about.links.partners"
},
"branding": {
"link": "/about/branding",
"label": "components.navigation.about.links.branding"
Expand Down
Loading
Loading