Skip to content

Commit

Permalink
Add ProductCard (#1870)
Browse files Browse the repository at this point in the history
  • Loading branch information
hasparus authored Dec 12, 2024
1 parent daed30b commit 8a38ca4
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 90 deletions.
5 changes: 5 additions & 0 deletions .changeset/short-donkeys-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@theguild/components': minor
---

Add ProductCard
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { cn } from '../cn';
import { FOUR_MAIN_PRODUCTS } from '../products';
import { Heading } from './heading';
import { ArrowIcon } from './icons';
import { MainProductCard } from './product-card';
import { TextLink } from './text-link';
import { MainProductCard } from './tools-and-libraries-cards';

export type ExploreMainProductCardsProps = HTMLAttributes<HTMLDivElement>;

Expand Down
7 changes: 2 additions & 5 deletions packages/components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,12 @@ export {
type GraphQLConfCardProps,
} from './hive-navigation';
export { HiveFooter } from './hive-footer';
export {
ToolsAndLibrariesCards,
MainProductCard,
AncillaryProductCard,
} from './tools-and-libraries-cards';
export { ToolsAndLibrariesCards } from './tools-and-libraries-cards';
export * from './decorations';
export * from './call-to-action';
export * from './cookies-consent';
export * from './heading';
export * from './info-card';
export * from './stud';
export * from './explore-main-product-cards';
export * from './product-card';
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 92 additions & 0 deletions packages/components/src/components/product-card/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { cn } from '../../cn';
import { FOUR_MAIN_PRODUCTS, ProductInfo, PRODUCTS } from '../../products';
import { HighlightDecoration } from '../decorations';
import { ArrowIcon } from '../icons';
import { ReactComponent as HiveDecoration } from './hive-decoration.svg';
import { ReactComponent as HiveGatewayDecoration } from './hive-gateway-decoration.svg';
import { ReactComponent as MeshDecoration } from './mesh-decoration.svg';
import { ReactComponent as YogaDecoration } from './yoga-decoration.svg';

const cardDecorations = {
[PRODUCTS.HIVE.name]: HiveDecoration,
[PRODUCTS.YOGA.name]: YogaDecoration,
[PRODUCTS.MESH.name]: MeshDecoration,
[PRODUCTS.HIVE_GATEWAY.name]: HiveGatewayDecoration,
};

export function MainProductCard({ as: Root, product, className, ...rest }: ProductCardProps) {
const Decoration = cardDecorations[product.name];
const Icon = product.logo;

const isHive = product.name === PRODUCTS.HIVE.name;

return (
<Root
className={cn(
'hive-focus-within group relative flex-1 shrink-0 basis-[283.5px] overflow-hidden rounded-2xl bg-blue-400 text-green-1000 max-md:w-[283.5px]',
isHive && 'bg-green-1000 text-white',
className,
)}
{...rest}
>
<a
className="relative z-10 flex h-full flex-1 flex-col justify-between p-8 outline-none focus-visible:outline-none"
href={product.href}
>
<p className="font-medium">{product.name}</p>
<Icon className="mt-8" />
<ArrowIcon className="absolute bottom-8 right-8" />
</a>
<Decoration
strokeWidth="0.5px"
className={cn(
'pointer-events-none absolute bottom-0 right-0 h-full fill-blue-200 opacity-0 transition-opacity duration-500 group-focus-within:opacity-100 group-hover:opacity-100',
isHive && 'fill-blue-700',
)}
preserveAspectRatio="xMidYMid meet"
/>
<HighlightDecoration className="pointer-events-none absolute left-0 top-[-15%] h-[150%] w-full opacity-0 transition-opacity duration-1000 group-focus-within:opacity-100 group-hover:opacity-100" />
</Root>
);
}

export function AncillaryProductCard({ product, as: Root, className, ...rest }: ProductCardProps) {
const Logo = product.logo;
return (
<Root
className={cn(
'hive-focus-within shrink-0 basis-[283.5px] rounded-2xl bg-beige-200 text-green-1000 transition-colors duration-500 hover:bg-beige-400 max-sm:min-w-[283.5px]',
className,
)}
{...rest}
>
<a
href={product.href}
className="relative flex h-full flex-col justify-between rounded-[inherit] p-8 focus:outline-none focus-visible:outline-none"
>
<div>
<p className="font-medium">{product.name}</p>
<p className="mt-2 text-sm text-green-800">{product.title}</p>
</div>
<div
role="presentation"
className="mt-8 flex size-8 items-center justify-center rounded bg-green-1000 p-[5px] text-sm/5 font-medium text-white"
>
<Logo />
</div>
<ArrowIcon className="absolute bottom-8 right-8" />
</a>
</Root>
);
}

export interface ProductCardProps extends React.HTMLAttributes<HTMLElement> {
as: 'div' | 'li';
product: ProductInfo;
}

export function ProductCard(props: ProductCardProps) {
const isMainProduct = FOUR_MAIN_PRODUCTS.some(p => p.name === props.product.name);

return isMainProduct ? <MainProductCard {...props} /> : <AncillaryProductCard {...props} />;
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Meta, StoryObj } from '@storybook/react';
import { hiveThemeDecorator } from '../../../../../.storybook/hive-theme-decorator';
import { PRODUCTS } from '../../products';
import { ProductCard } from './index';

export default {
title: 'Hive/ProductCard',
component: ProductCard,
decorators: [hiveThemeDecorator],
parameters: {
forcedLightMode: true,
},
} satisfies Meta;

// interweaved to make sure main product cards look good alongside ancillary product cards
const productsLexicographically = Object.values(PRODUCTS).sort((a, b) =>
a.name.localeCompare(b.name),
);

export const Default: StoryObj<typeof ProductCard> = {
name: 'ProductCard',
render() {
return (
<ul className="mt-5 grid grid-cols-4 gap-5 overflow-x-auto p-4 last-of-type:mb-24">
{productsLexicographically.map(product => (
<ProductCard as="li" key={product.name} product={product} />
))}
</ul>
);
},
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
import { cn } from '../../cn';
import {
FOUR_MAIN_PRODUCTS,
ProductInfo,
PRODUCTS,
SIX_HIGHLIGHTED_PRODUCTS,
} from '../../products';
import { FOUR_MAIN_PRODUCTS, SIX_HIGHLIGHTED_PRODUCTS } from '../../products';
import { CallToAction } from '../call-to-action';
import { HighlightDecoration } from '../decorations';
import { Heading } from '../heading';
import { ArrowIcon } from '../icons';
import { ReactComponent as HiveDecoration } from './hive-decoration.svg';
import { ReactComponent as HiveGatewayDecoration } from './hive-gateway-decoration.svg';
import { ReactComponent as MeshDecoration } from './mesh-decoration.svg';
import { ReactComponent as YogaDecoration } from './yoga-decoration.svg';
import { AncillaryProductCard, MainProductCard } from '../product-card';

const cardDecorations = {
[PRODUCTS.HIVE.name]: HiveDecoration,
[PRODUCTS.YOGA.name]: YogaDecoration,
[PRODUCTS.MESH.name]: MeshDecoration,
[PRODUCTS.HIVE_GATEWAY.name]: HiveGatewayDecoration,
};

export function ToolsAndLibrariesCards({ className }: { className?: string }) {
export function ToolsAndLibrariesCards({
className,
isHive,
}: {
className?: string;
isHive?: boolean;
}) {
return (
<section
className={cn(
Expand All @@ -44,68 +33,12 @@ export function ToolsAndLibrariesCards({ className }: { className?: string }) {
<AncillaryProductCard key={product.name} as="li" product={product} />
))}
</ul>
<CallToAction href="https://github.com/the-guild-org" variant="primary">
<CallToAction
href={isHive ? '/ecosystem' : 'https://the-guild.dev/graphql/hive/ecosystem'}
variant="primary"
>
Explore the Ecosystem
</CallToAction>
</section>
);
}

export function MainProductCard({ as: Root, product }: { as: 'div' | 'li'; product: ProductInfo }) {
const Decoration = cardDecorations[product.name];
const Icon = product.logo;

return (
<Root
key={product.name}
className="hive-focus-within group relative flex-1 shrink-0 basis-[283.5px] overflow-hidden rounded-2xl bg-blue-400 text-green-1000 first-of-type:bg-green-1000 first-of-type:text-white max-md:w-[283.5px]"
>
<a
className="relative z-10 block flex-1 p-8 outline-none focus-visible:outline-none"
href={product.href}
>
<p className="font-medium">{product.name}</p>
<Icon className="mt-8" />
<ArrowIcon className="absolute bottom-8 right-8" />
</a>
<Decoration
strokeWidth="0.5px"
className="pointer-events-none absolute bottom-0 right-0 fill-blue-200 opacity-0 transition-opacity duration-500 group-first-of-type:fill-blue-700 group-focus-within:opacity-100 group-hover:opacity-100"
preserveAspectRatio="xMidYMid meet"
/>
<HighlightDecoration className="pointer-events-none absolute left-0 top-[-15%] h-[150%] w-full opacity-0 transition-opacity duration-1000 group-focus-within:opacity-100 group-hover:opacity-100" />
</Root>
);
}

export function AncillaryProductCard({
product,
as: Root,
}: {
product: ProductInfo;
as: 'div' | 'li';
}) {
const Logo = product.logo;
return (
<Root
key={product.name}
className="hive-focus-within shrink-0 basis-[283.5px] rounded-2xl bg-beige-200 text-green-1000 transition-colors duration-500 hover:bg-beige-400 max-sm:min-w-[283.5px]"
>
<a
href={product.href}
className="relative flex h-full flex-col rounded-[inherit] p-8 focus:outline-none focus-visible:outline-none"
>
<p className="font-medium">{product.name}</p>
<p className="mt-2 text-sm text-green-800">{product.title}</p>
<div className="h-8 grow" />
<div
role="presentation"
className="flex size-8 items-center justify-center rounded bg-green-1000 p-[5px] text-sm/5 font-medium text-white"
>
<Logo />
</div>
<ArrowIcon className="absolute bottom-8 right-8" />
</a>
</Root>
);
}

0 comments on commit 8a38ca4

Please sign in to comment.