-
-
Notifications
You must be signed in to change notification settings - Fork 6.4k
chore: partners/sponsors page #7991
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bjohansebas
wants to merge
21
commits into
main
Choose a base branch
from
partner-pages
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
33dcf64
chore: add partner section on homepage
bjohansebas 5094798
chore: template for partner page
bjohansebas 6a5568c
ui: add name tooltip for partnerIcon
bjohansebas e646e29
chore: implement list of logos on download and partners page
bjohansebas 8302e8a
chore: add partner list on security blog post
bjohansebas 2f0e2be
feat: enhance PartnersLogoList to filter partners by category and all…
bjohansebas 435eb91
feat: add new partner categories and update partner weights
bjohansebas 5fd31f8
feat: update partner selection logic to use weighted randomization
bjohansebas 91b6535
feat: update button href to include UTM parameters for tracking
bjohansebas 5c79be3
feat: implement padding to a company tooltip
bjohansebas 8c55aaf
chore: implement supporters component
bjohansebas 86cebfa
feat: enhance partners page with detailed descriptions
bjohansebas 9570258
ui-components: add more partner logos
bjohansebas 96bc9f9
ui-components: add more partners logos
bjohansebas edf5259
clean up
bjohansebas 625ed9c
feat: update supporters and partners sections
bjohansebas cdaa6d6
feat: enhance partner list functionality with sorting options
bjohansebas d737364
fix typos and small nits
bjohansebas 4da9fad
refactor: update partners and supporters sections by removing unused …
bjohansebas 26c9f8c
feat: add comprehensive partners documentation outlining addition, re…
bjohansebas 844cd09
style: center-align partner support heading and adjust text balance
bjohansebas File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
13 changes: 13 additions & 0 deletions
13
apps/site/components/Common/Partners/PartnerIcon/index.module.css
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
apps/site/components/Common/Partners/PartnerIcon/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
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={<span className="px-2">{name}</span>}> | ||
<Button | ||
kind="secondary" | ||
href={`${href}/?utm_source=nodejs-website&utm_medium=Link`} | ||
className={style.partnerIcon} | ||
> | ||
{cloneElement(logo, { | ||
width: 'auto', | ||
height: '16px', | ||
})} | ||
</Button> | ||
</Tooltip> | ||
</Skeleton> | ||
); | ||
|
||
export default PartnersIcon; |
19 changes: 19 additions & 0 deletions
19
apps/site/components/Common/Partners/PartnerLogo/index.module.css
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
27
apps/site/components/Common/Partners/PartnerLogo/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: 'auto', | ||
height: '16px', | ||
})} | ||
</Button> | ||
</Skeleton> | ||
); | ||
|
||
export default PartnersLogo; |
9 changes: 9 additions & 0 deletions
9
apps/site/components/Common/Partners/PartnersIconList/index.module.css
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
69 changes: 69 additions & 0 deletions
69
apps/site/components/Common/Partners/PartnersIconList/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
'use client'; | ||
|
||
import { useEffect, useRef, useState } from 'react'; | ||
import type { FC } from 'react'; | ||
|
||
import { ICON_PARTNERS } from '#site/next.partners.constants'; | ||
import type { PartnerCategory, Partners } from '#site/types'; | ||
|
||
import PartnerIcon from '../PartnerIcon'; | ||
import style from './index.module.css'; | ||
import { randomPartnerList } from '../utils'; | ||
|
||
type PartnersIconListProps = { | ||
maxLength?: number; | ||
categories?: PartnerCategory; | ||
}; | ||
|
||
const PartnersIconList: FC<PartnersIconListProps> = ({ | ||
maxLength = 6, | ||
categories, | ||
}) => { | ||
const initialRenderer = useRef(true); | ||
|
||
const [seedList, setSeedList] = useState<Array<Partners>>( | ||
ICON_PARTNERS.slice(0, maxLength) | ||
); | ||
|
||
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( | ||
randomPartnerList(ICON_PARTNERS, { | ||
pick: maxLength, | ||
dateSeed: 5, | ||
category: categories, | ||
}) | ||
); | ||
}, 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 ( | ||
<div className={style.partnersIconList}> | ||
{seedList.map((partner, index) => ( | ||
<PartnerIcon | ||
{...partner} | ||
key={index} | ||
loading={initialRenderer.current} | ||
/> | ||
))} | ||
</div> | ||
); | ||
}; | ||
|
||
export default PartnersIconList; |
8 changes: 8 additions & 0 deletions
8
apps/site/components/Common/Partners/PartnersLogoList/index.module.css
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
77 changes: 77 additions & 0 deletions
77
apps/site/components/Common/Partners/PartnersLogoList/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
'use client'; | ||
|
||
import { useEffect, useRef, useState } from 'react'; | ||
import type { FC } from 'react'; | ||
|
||
import { LOGO_PARTNERS } from '#site/next.partners.constants'; | ||
import type { PartnerCategory, Partners } from '#site/types'; | ||
|
||
import PartnerLogo from '../PartnerLogo'; | ||
import style from './index.module.css'; | ||
import { randomPartnerList } from '../utils'; | ||
|
||
type PartnersLogoListProps = { | ||
maxLength?: number; | ||
categories?: PartnerCategory; | ||
sort?: 'name' | 'weight'; | ||
}; | ||
|
||
const PartnersLogoList: FC<PartnersLogoListProps> = ({ | ||
maxLength = 3, | ||
sort = 'weight', | ||
categories, | ||
}) => { | ||
const initialRenderer = useRef(true); | ||
|
||
const [seedList, setSeedList] = useState<Array<Partners>>(() => { | ||
if (maxLength === null) { | ||
return LOGO_PARTNERS.filter( | ||
partner => !categories || partner.categories.includes(categories) | ||
); | ||
} | ||
return LOGO_PARTNERS.slice(0, maxLength); | ||
}); | ||
|
||
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( | ||
randomPartnerList(LOGO_PARTNERS, { | ||
pick: maxLength, | ||
dateSeed: 5, | ||
category: categories, | ||
sort: 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 ( | ||
<div className={style.partnersLogoList}> | ||
{seedList.map((partner, index) => ( | ||
<PartnerLogo | ||
{...partner} | ||
key={index} | ||
loading={initialRenderer.current} | ||
/> | ||
))} | ||
</div> | ||
); | ||
}; | ||
|
||
export default PartnersLogoList; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import type { PartnerCategory, Partners } from '#site/types/partners.js'; | ||
|
||
function randomPartnerList( | ||
partners: Array<Partners>, | ||
config: { | ||
/** | ||
* Number of partners to pick from the list. | ||
* If null, all partners will be returned. | ||
*/ | ||
pick?: number | null; | ||
/** | ||
* Date seed to use for the randomization. | ||
* This is used to ensure that the same partners are returned for the same date. | ||
*/ | ||
dateSeed?: number; | ||
/** | ||
* Category of partners to filter by. | ||
* If not provided, all partners will be returned. | ||
*/ | ||
category?: PartnerCategory; | ||
/** | ||
* Whether to randomize the partners or not. | ||
*/ | ||
sort?: 'name' | 'weight' | null; | ||
} | ||
) { | ||
const { pick = 4, dateSeed = 5, category, sort = 'weight' } = config; | ||
|
||
const filteredPartners = [...partners].filter(partner => { | ||
return !category || partner.categories.includes(category); | ||
}); | ||
|
||
if (sort === null) { | ||
return pick !== null ? filteredPartners.slice(0, pick) : filteredPartners; | ||
} | ||
|
||
if (sort === 'name') { | ||
const shuffled = [...filteredPartners].sort((a, b) => | ||
a.name.localeCompare(b.name) | ||
); | ||
|
||
return pick !== null ? shuffled.slice(0, pick) : shuffled; | ||
} | ||
|
||
const now = new Date(); | ||
const minutes = Math.floor(now.getUTCMinutes() / dateSeed) * dateSeed; | ||
|
||
const fixedTime = new Date( | ||
Date.UTC( | ||
now.getUTCFullYear(), | ||
now.getUTCMonth(), | ||
now.getUTCDate(), | ||
now.getUTCHours(), | ||
minutes, | ||
0, | ||
0 | ||
) | ||
); | ||
|
||
// We create a seed from the rounded date (timestamp in ms) | ||
const seed = fixedTime.getTime(); | ||
const rng = mulberry32(seed); | ||
|
||
const weightedPartners = filteredPartners.flatMap(partner => { | ||
const weight = partner.weight; | ||
return Array(weight).fill(partner); | ||
}); | ||
|
||
// Create a copy of the array to avoid modifying the original | ||
const shuffled = [...weightedPartners].sort(() => rng() - 0.5); | ||
|
||
// Remove duplicates while preserving order | ||
const unique = Array.from(new Set(shuffled)); | ||
|
||
if (pick !== null) { | ||
return unique.slice(0, pick); | ||
} | ||
|
||
return unique; | ||
} | ||
|
||
// This function returns a random list of partners based on a fixed time seed | ||
function mulberry32(seed: number) { | ||
return function () { | ||
let t = (seed += 0x6d2b79f5); | ||
t = Math.imul(t ^ (t >>> 15), t | 1); | ||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61); | ||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296; | ||
}; | ||
} | ||
|
||
export { randomPartnerList }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
'use client'; | ||
|
||
import Avatar from '@node-core/ui-components/Common/AvatarGroup/Avatar'; | ||
import type { FC } from 'react'; | ||
import { use } from 'react'; | ||
|
||
import type { Supporters } from '#site/types'; | ||
|
||
type SupportersProps = { | ||
supporters: Promise<Array<Supporters>>; | ||
}; | ||
|
||
// TODO: Sort supporters by all time contribution amount and link to their Open Collective page | ||
const SupportersList: FC<SupportersProps> = ({ supporters }) => { | ||
const supportersList = use(supporters); | ||
|
||
return ( | ||
<div className="flex max-w-full flex-wrap items-center justify-center gap-1"> | ||
{supportersList.map(({ name, image }, i) => ( | ||
<Avatar nickname={name} image={image} key={`${name}-${i}`} /> | ||
))} | ||
</div> | ||
); | ||
}; | ||
|
||
export default SupportersList; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import type { FC, PropsWithChildren } from 'react'; | ||
|
||
import { fetchOpenCollectiveData } from '#site/next-data/generators/supportersData.mjs'; | ||
import type { Supporters } from '#site/types'; | ||
|
||
import SupportersList from './Common/Supporters'; | ||
|
||
const WithSupporters: FC<PropsWithChildren> = () => { | ||
const supporters = fetchOpenCollectiveData() as Promise<Array<Supporters>>; | ||
|
||
return ( | ||
<div className="flex max-w-full flex-wrap items-center gap-1"> | ||
<SupportersList supporters={supporters} /> | ||
</div> | ||
); | ||
}; | ||
|
||
export default WithSupporters; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.