mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
203 lines
6.3 KiB
TypeScript
203 lines
6.3 KiB
TypeScript
"use client";
|
|
|
|
import {FC, useMemo, useRef} from "react";
|
|
import {Avatar, AvatarProps, Button, Spacer, Tooltip} from "@nextui-org/react";
|
|
import {clamp, get} from "lodash";
|
|
|
|
import {sectionWrapper, titleWrapper, title, subtitle} from "../primitives";
|
|
|
|
import {FeaturesGrid} from "@/components/marketing";
|
|
import {OpenCollectiveIcon, PatreonIcon, HeartBoldIcon, PlusLinearIcon} from "@/components/icons";
|
|
import {Sponsor, SPONSOR_TIERS, SPONSOR_COLORS, getTier} from "@/libs/docs/sponsors";
|
|
import {SonarPulse} from "@/components/sonar-pulse";
|
|
import {useIsMobile} from "@/hooks/use-media-query";
|
|
|
|
export interface SupportProps {
|
|
sponsors: Sponsor[];
|
|
}
|
|
|
|
const supportAccounts = [
|
|
{
|
|
title: "Open Collective",
|
|
description: "Sponsor the NextUI maintainers.",
|
|
icon: <OpenCollectiveIcon className="text-pink-500" />,
|
|
href: "https://opencollective.com/nextui",
|
|
isExternal: true,
|
|
},
|
|
{
|
|
title: "Patreon",
|
|
description: "Sponsor the creator, Junior Garcia.",
|
|
icon: <PatreonIcon className="text-pink-500" />,
|
|
href: "https://www.patreon.com/jrgarciadev?fan_landing=true",
|
|
isExternal: true,
|
|
},
|
|
];
|
|
|
|
const SONAR_PULSE_SIZE = 80;
|
|
const SONAR_PULSE_CIRCLES_COUNT = 4;
|
|
const SONAR_PULSE_RADIUS = 130;
|
|
|
|
const getSponsorName = (sponsor: Sponsor) => {
|
|
if (!sponsor.name) {
|
|
return "";
|
|
}
|
|
|
|
return sponsor.name.slice(0, 2).toUpperCase();
|
|
};
|
|
|
|
const getSponsorSize = (sponsor: Sponsor, isMobile: boolean) => {
|
|
let size: AvatarProps["size"] = "md";
|
|
const tier = sponsor.tier || getTier(sponsor.totalAmountDonated);
|
|
|
|
switch (tier) {
|
|
case SPONSOR_TIERS.BRONZE:
|
|
size = isMobile ? "sm" : "md";
|
|
break;
|
|
case SPONSOR_TIERS.SILVER:
|
|
size = isMobile ? "sm" : "md";
|
|
break;
|
|
case SPONSOR_TIERS.GOLD:
|
|
size = isMobile ? "lg" : "xl";
|
|
break;
|
|
case SPONSOR_TIERS.PLATINUM:
|
|
size = isMobile ? "lg" : "xl";
|
|
break;
|
|
default:
|
|
size = isMobile ? "sm" : "md";
|
|
}
|
|
|
|
return size;
|
|
};
|
|
|
|
const getSponsorColor = (sponsor: Sponsor) => {
|
|
const tier = sponsor.tier || getTier(sponsor.totalAmountDonated);
|
|
|
|
return SPONSOR_COLORS[tier] || "default";
|
|
};
|
|
|
|
const getSponsorAvatarStyles = (index: number, sponsors: Sponsor[] = []) => {
|
|
const angle = (index * 360) / sponsors.length;
|
|
const radius = SONAR_PULSE_RADIUS;
|
|
|
|
// position the avatar randomly inside the sonar pulse
|
|
const randomRadius = clamp(Math.floor((index + 1) * radius), radius * 0.4, radius);
|
|
|
|
const x = randomRadius * Math.cos((angle * Math.PI) / 180);
|
|
const y = randomRadius * Math.sin((angle * Math.PI) / 180);
|
|
|
|
return {
|
|
transform: `translate(${x}px, ${y}px)`,
|
|
};
|
|
};
|
|
|
|
export const Support: FC<SupportProps> = ({sponsors = []}) => {
|
|
const sonarRef = useRef(null);
|
|
const isMobile = useIsMobile();
|
|
|
|
const handleExternalLinkClick = (href: string) => {
|
|
if (!href) return;
|
|
window.open(href, "_blank");
|
|
};
|
|
|
|
const renderSponsors = useMemo(() => {
|
|
if (!sponsors.length) return null;
|
|
|
|
return (
|
|
<div
|
|
className="absolute rounded-full bg-transparent"
|
|
style={{
|
|
width: `${SONAR_PULSE_RADIUS}px`,
|
|
top: SONAR_PULSE_RADIUS / 6,
|
|
left: SONAR_PULSE_RADIUS / 6,
|
|
}}
|
|
>
|
|
{sponsors.map((sponsor, index) => (
|
|
<Avatar
|
|
key={`${sponsor.MemberId}-${index}`}
|
|
isBordered
|
|
showFallback
|
|
className="absolute cursor-pointer bg-transparent before:bg-white/10 before:content-[''] before:block before:z-[-1] before:absolute before:inset-0 before:backdrop-blur-md before:backdrop-saturate-200"
|
|
color={getSponsorColor(sponsor) as AvatarProps["color"]}
|
|
name={getSponsorName(sponsor)}
|
|
size={getSponsorSize(sponsor, isMobile)}
|
|
src={sponsor.image}
|
|
style={getSponsorAvatarStyles(index, sponsors)}
|
|
onClick={() =>
|
|
handleExternalLinkClick(get(sponsor, "website") || get(sponsor, "profile"))
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}, [isMobile, sponsors]);
|
|
|
|
return (
|
|
<section className={sectionWrapper({class: "flex flex-col items-center z-20 mt-16 lg:mt-44"})}>
|
|
<div className="max-w-4xl flex flex-col gap-8">
|
|
<div>
|
|
<div className={titleWrapper({class: "text-center items-center"})}>
|
|
<div className="flex md:inline-flex flex-col md:flex-row items-center">
|
|
<h1 className={title({size: "lg"})}>Support NextUI </h1>
|
|
<HeartBoldIcon
|
|
className="text-pink-500 animate-heartbeat"
|
|
size={50}
|
|
style={{
|
|
animationDuration: "2.5s",
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<p
|
|
className={subtitle({class: "md:w-full text-center flex justify-center items-center"})}
|
|
>
|
|
Using NextUI in a profit-making product, as a freelancer, or for fun projects? Your
|
|
contributions will help to make NextUI better.
|
|
</p>
|
|
<Spacer y={12} />
|
|
<FeaturesGrid
|
|
classNames={{
|
|
base: "lg:grid-cols-2",
|
|
}}
|
|
features={supportAccounts}
|
|
/>
|
|
<div
|
|
ref={sonarRef}
|
|
className="relative mt-32 md:mt-60 w-full flex items-center justify-center"
|
|
>
|
|
<SonarPulse
|
|
circlesCount={SONAR_PULSE_CIRCLES_COUNT}
|
|
color="#7928CA"
|
|
icon={
|
|
<Tooltip
|
|
showArrow
|
|
color="secondary"
|
|
content={"Become a sponsor"}
|
|
offset={10}
|
|
radius="xl"
|
|
>
|
|
<Button
|
|
isIconOnly
|
|
aria-label="Become a sponsor"
|
|
className="z-50 w-auto h-auto bg-gradient-to-b from-[#FF1CF7] to-[#7928CA]"
|
|
radius="full"
|
|
onPress={() => handleExternalLinkClick(supportAccounts[0].href)}
|
|
>
|
|
<PlusLinearIcon
|
|
className="flex items-center justify-center rounded-full text-white"
|
|
size={54}
|
|
/>
|
|
</Button>
|
|
</Tooltip>
|
|
}
|
|
playState="running"
|
|
size={SONAR_PULSE_SIZE}
|
|
>
|
|
{renderSponsors}
|
|
</SonarPulse>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|