-
Notifications
You must be signed in to change notification settings - Fork 399
upcoming: [UIE-9818, UIE-9820, UIE-9821, UIE-9822] - Implement product details for Marketplace #13271
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
upcoming: [UIE-9818, UIE-9820, UIE-9821, UIE-9822] - Implement product details for Marketplace #13271
Changes from all commits
68565e3
cc3b981
9301f2f
1460ada
eb78c8d
105ec0a
c1900c3
ccff7a0
107958c
39bee9f
6504111
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@linode/manager": Upcoming Features | ||
| --- | ||
|
|
||
| Marketplace details and added tabs to the Products details page ([#13271](https://github.com/linode/manager/pull/13271)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { Box, Chip, Notice } from '@linode/ui'; | ||
| import { styled } from '@mui/material/styles'; | ||
|
|
||
| export const ProductDetailsContainer = styled(Box)(({ theme }) => ({ | ||
| alignItems: 'flex-start', | ||
|
Check warning on line 5 in packages/manager/src/features/Marketplace/ProductDetails/ProductDetails.styles.ts
|
||
| alignSelf: 'stretch', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| gap: theme.spacingFunction(32), | ||
| paddingLeft: theme.spacingFunction(8), | ||
| paddingRight: theme.spacingFunction(8), | ||
| paddingTop: theme.spacingFunction(8), | ||
| })); | ||
|
|
||
| export const InfoBanner = styled(Notice)(() => ({ | ||
| alignItems: 'flex-start', | ||
| display: 'flex', | ||
| maxWidth: '630px', | ||
| width: '100%', | ||
| marginBottom: 0, | ||
| })); | ||
|
|
||
| export const ProductInfoSection = styled(Box)(({ theme }) => ({ | ||
| alignItems: 'flex-start', | ||
| alignSelf: 'stretch', | ||
| display: 'flex', | ||
| gap: theme.spacingFunction(24), | ||
| })); | ||
|
|
||
| export const LogoContainer = styled(Box)(({ theme }) => ({ | ||
| alignItems: 'center', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| height: theme.spacingFunction(96), | ||
| justifyContent: 'center', | ||
| width: theme.spacingFunction(96), | ||
| })); | ||
|
|
||
| export const ProductDetailsSection = styled(Box)(({ theme }) => ({ | ||
| alignItems: 'flex-start', | ||
| alignSelf: 'stretch', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| gap: theme.spacingFunction(16), | ||
| })); | ||
|
|
||
| export const ProductTitleSection = styled(Box)(({ theme }) => ({ | ||
| alignItems: 'flex-start', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| gap: theme.spacingFunction(2), | ||
| })); | ||
|
|
||
| export const TagsContainer = styled(Box)(({ theme }) => ({ | ||
| alignItems: 'center', | ||
| display: 'flex', | ||
| flexWrap: 'wrap', | ||
| gap: theme.spacingFunction(8), | ||
| })); | ||
|
|
||
| export const StyledChip = styled(Chip)(({ theme }) => ({ | ||
| '& .MuiChip-label': { | ||
| font: theme.font.bold, | ||
| fontSize: theme.tokens.font.FontSize.Xxxs, | ||
| letterSpacing: '0.12px', | ||
| lineHeight: '12px', | ||
| padding: `${theme.spacingFunction(4)} ${theme.spacingFunction(6)}`, | ||
| }, | ||
| flexShrink: 0, | ||
| })); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| import { Box, Button, ErrorState, Paper, Typography } from '@linode/ui'; | ||
| import { useTheme } from '@mui/material/styles'; | ||
| import { useParams } from '@tanstack/react-router'; | ||
| import * as React from 'react'; | ||
|
|
||
| import { Markdown } from 'src/components/Markdown/Markdown'; | ||
|
|
||
| import { getProductById } from '../products'; | ||
| import { getLogoUrl } from '../shared'; | ||
| import { getProductTabDetails } from './pages'; | ||
| import { | ||
| InfoBanner, | ||
| LogoContainer, | ||
| ProductDetailsContainer, | ||
| ProductDetailsSection, | ||
| ProductInfoSection, | ||
| ProductTitleSection, | ||
| StyledChip, | ||
| TagsContainer, | ||
| } from './ProductDetails.styles'; | ||
| import { ProductDetailsTabs } from './ProductDetailsTabs'; | ||
|
|
||
| /** | ||
| * Main Product Details Component | ||
| */ | ||
| export const ProductDetails = () => { | ||
| const { productId } = useParams({ | ||
| from: '/cloud-marketplace/catalog/$productId', | ||
| }); | ||
| const theme = useTheme(); | ||
|
|
||
| const product = React.useMemo(() => getProductById(productId), [productId]); | ||
|
|
||
| // Get logo URL based on theme | ||
| const logoUrl = React.useMemo(() => { | ||
| if (!product) { | ||
| return ''; | ||
| } | ||
| return getLogoUrl(product, theme); | ||
| }, [product, theme]); | ||
|
|
||
| // Handle invalid/unknown product id | ||
| if (!product) { | ||
| return ( | ||
| <ErrorState | ||
| errorText={'Unable to load product details. Please try again later.'} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| // Tab content is optional. If not present for this product, we still show the page. | ||
| const details = getProductTabDetails(productId); | ||
|
|
||
| // Contact sales handler placeholder - will be implemented in a future ticket | ||
| const handleContactSales = () => { | ||
| // Placeholder for contact sales functionality | ||
| }; | ||
|
|
||
| return ( | ||
| <Paper | ||
| sx={(theme) => ({ | ||
| alignItems: 'flex-start', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| mx: { | ||
| md: 0, | ||
| sm: theme.spacingFunction(16), | ||
| xs: theme.spacingFunction(12), | ||
| }, | ||
| })} | ||
| > | ||
| <ProductDetailsContainer> | ||
| {/* Info Banner (conditional) */} | ||
| {product.infoBanner && ( | ||
| <InfoBanner variant="info"> | ||
| <Markdown textOrMarkdown={product.infoBanner} /> | ||
| </InfoBanner> | ||
| )} | ||
|
|
||
| {/* Product Info Section */} | ||
| <ProductInfoSection> | ||
| {/* Product Logo */} | ||
| <LogoContainer> | ||
| {logoUrl && ( | ||
| <img | ||
| alt={`${product.name} logo`} | ||
| src={logoUrl} | ||
| style={{ | ||
| height: '100%', | ||
| objectFit: 'contain', | ||
| width: '100%', | ||
| }} | ||
| /> | ||
| )} | ||
| </LogoContainer> | ||
|
|
||
| {/* Product Details */} | ||
| <ProductDetailsSection> | ||
| {/* Product Name and Partner */} | ||
| <ProductTitleSection> | ||
| <Typography | ||
| sx={(theme) => ({ | ||
| color: theme.tokens.alias.Content.Text.Primary.Default, | ||
| font: theme.font.extrabold, | ||
| })} | ||
| variant="h1" | ||
| > | ||
| {product.name} | ||
| </Typography> | ||
| {product.partner && ( | ||
| <Typography | ||
| sx={(theme) => ({ | ||
| color: theme.tokens.alias.Content.Text.Secondary.Default, | ||
| font: theme.font.bold, | ||
| })} | ||
| variant="body1" | ||
| > | ||
| {product.partner.name} | ||
| </Typography> | ||
| )} | ||
| </ProductTitleSection> | ||
|
|
||
| {/* Description */} | ||
| <Typography | ||
| sx={(theme) => ({ | ||
| alignSelf: 'stretch', | ||
| color: theme.tokens.component.Tile.Default.Text, | ||
| font: theme.font.normal, | ||
| maxWidth: '800px', | ||
| })} | ||
| variant="body1" | ||
| > | ||
| {product.shortDescription} | ||
| </Typography> | ||
|
|
||
| {/* Tags */} | ||
| <TagsContainer> | ||
| {/* Tile Tag */} | ||
| {product.tileTag && ( | ||
| <StyledChip | ||
| label={product.tileTag} | ||
| sx={(theme) => ({ | ||
| backgroundColor: | ||
| theme.tokens.component.Badge.Positive.Subtle.Background, | ||
| color: theme.tokens.component.Badge.Positive.Subtle.Text, | ||
| })} | ||
| /> | ||
| )} | ||
|
|
||
| {/* Product Tags */} | ||
| {product.productTags?.map((tag: string, index: number) => ( | ||
| <StyledChip | ||
| key={index} | ||
| label={tag} | ||
| sx={(theme) => ({ | ||
| backgroundColor: | ||
| theme.tokens.component.Badge.Informative.Subtle | ||
| .Background, | ||
| color: theme.tokens.component.Badge.Informative.Subtle.Text, | ||
| })} | ||
| /> | ||
| ))} | ||
| </TagsContainer> | ||
|
|
||
| {/* Contact Sales Button */} | ||
| <Box marginTop={1}> | ||
| <Button | ||
| buttonType="primary" | ||
| data-pendo-id={`Cloud Marketplace ${product.name}-Contact Sales`} | ||
| onClick={handleContactSales} | ||
| > | ||
| Contact Sales | ||
| </Button> | ||
| </Box> | ||
| </ProductDetailsSection> | ||
| </ProductInfoSection> | ||
|
|
||
| {/* Product Details Tabs */} | ||
| {details && ( | ||
| <Box width="100%"> | ||
| <ProductDetailsTabs details={details} /> | ||
| </Box> | ||
| )} | ||
| </ProductDetailsContainer> | ||
| </Paper> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { Box } from '@linode/ui'; | ||
| import { styled } from '@mui/material/styles'; | ||
|
|
||
| /** | ||
| * Styled components for Overview tab layout | ||
| */ | ||
| export const OverviewContainer = styled(Box)(({ theme }) => ({ | ||
| alignItems: 'flex-start', | ||
| alignSelf: 'stretch', | ||
| display: 'flex', | ||
| gap: '24px', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we use
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same with gaps |
||
| justifyContent: 'space-between', | ||
| [theme.breakpoints.down('md')]: { | ||
| flexDirection: 'column', | ||
| gap: '48px', | ||
| }, | ||
| })); | ||
|
|
||
| export const VideoPlaceholder = styled(Box)(({ theme }) => ({ | ||
| alignItems: 'center', | ||
| alignSelf: 'stretch', | ||
| aspectRatio: '3/2', | ||
| backgroundColor: theme.bg.bgPaper, | ||
| border: `1px dashed ${theme.tokens.alias.Border.Normal}`, | ||
| borderRadius: '12px', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| gap: theme.spacingFunction(8), | ||
| height: '202px', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we ok with a set height here rather than a responsive container? Also, are we serving videos locally? Pictures is one thing but this is concerning
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No videos will not be served locally. Currently there are no videos in beta. |
||
| justifyContent: 'center', | ||
| padding: '10px', | ||
| svg: { | ||
| fill: theme.tokens.alias.Content.Icon.Primary.Default, | ||
| opacity: 0.25, | ||
| }, | ||
| [theme.breakpoints.down('md')]: { | ||
| order: -1, | ||
| }, | ||
| })); | ||
|
|
||
| export const ContentSection = styled(Box)(() => ({ | ||
| flex: 1, | ||
| minWidth: 0, | ||
| })); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we can move this if-block before the productDetails are fetched?