Files
react-test/docs/pages/blog.tsx
how2ice 005cf56baf
Some checks failed
No response / noResponse (push) Has been cancelled
CI / Continuous releases (push) Has been cancelled
CI / test-dev (macos-latest) (push) Has been cancelled
CI / test-dev (ubuntu-latest) (push) Has been cancelled
CI / test-dev (windows-latest) (push) Has been cancelled
Maintenance / main (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
init project
2025-12-12 14:26:25 +09:00

478 lines
16 KiB
TypeScript

import * as React from 'react';
import { InferGetStaticPropsType } from 'next';
import { useRouter } from 'next/router';
import Avatar from '@mui/material/Avatar';
import AvatarGroup from '@mui/material/AvatarGroup';
import Box from '@mui/material/Box';
import Container from '@mui/material/Container';
import Divider from '@mui/material/Divider';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import Pagination from '@mui/material/Pagination';
import Button from '@mui/material/Button';
import KeyboardArrowRightRoundedIcon from '@mui/icons-material/KeyboardArrowRightRounded';
import Chip from '@mui/material/Chip';
import XIcon from '@mui/icons-material/X';
import GitHubIcon from '@mui/icons-material/GitHub';
import LinkedInIcon from '@mui/icons-material/LinkedIn';
import YouTubeIcon from '@mui/icons-material/YouTube';
import DiscordIcon from 'docs/src/icons/DiscordIcon';
import Head from 'docs/src/modules/components/Head';
import AppHeader from 'docs/src/layouts/AppHeader';
import AppFooter from 'docs/src/layouts/AppFooter';
import GradientText from 'docs/src/components/typography/GradientText';
import BrandingCssVarsProvider from 'docs/src/BrandingCssVarsProvider';
import { authors as AUTHORS } from 'docs/src/modules/components/TopLayoutBlog';
import HeroEnd from 'docs/src/components/home/HeroEnd';
import { Link } from '@mui/docs/Link';
import generateRssFeed from 'docs/scripts/generateRSSFeed';
import Section from 'docs/src/layouts/Section';
import SectionHeadline from 'docs/src/components/typography/SectionHeadline';
import { getAllBlogPosts, BlogPost } from 'docs/lib/sourcing';
export const getStaticProps = () => {
const data = getAllBlogPosts();
generateRssFeed(data.allBlogPosts);
return {
props: data,
};
};
function PostPreview(props: BlogPost) {
return (
<React.Fragment>
<Box sx={{ display: 'flex', gap: 0.5, mb: 1.5 }}>
{props.tags.map((tag) => (
<Chip
key={tag}
label={tag}
size="small"
variant="outlined"
color="primary"
sx={(theme) => ({
height: 22,
fontWeight: 'medium',
fontSize: theme.typography.pxToRem(13),
'& .MuiChip-label': {
px: '6px',
},
...theme.applyDarkStyles({
color: (theme.vars || theme).palette.grey[200],
}),
})}
/>
))}
</Box>
<Typography component="h2" variant="subtitle1" gutterBottom sx={{ fontWeight: 'bold' }}>
<Link
aria-describedby={`describe-${props.slug}`}
href={`/blog/${props.slug}/`}
sx={{
color: 'text.primary',
'&:hover': {
textDecoration: 'underline',
},
}}
>
{props.title}
</Link>
</Typography>
<Typography sx={{ color: 'text.secondary', mb: 'auto' }}>{props.description}</Typography>
{props.authors && (
<AvatarGroup
sx={(theme) => ({
mt: 2,
mb: 1,
alignSelf: 'flex-start',
'& .MuiAvatar-circular': {
width: 28,
height: 28,
fontSize: theme.typography.pxToRem(13),
fontWeight: theme.typography.fontWeightSemiBold,
color: (theme.vars || theme).palette.text.primary,
border: `1px solid ${(theme.vars || theme).palette.divider}`,
outline: '3px solid',
outlineColor: '#FFF',
backgroundColor: (theme.vars || theme).palette.grey[100],
},
...theme.applyDarkStyles({
'& .MuiAvatar-circular': {
outlineColor: (theme.vars || theme).palette.primaryDark[900],
backgroundColor: (theme.vars || theme).palette.primaryDark[700],
},
}),
})}
>
{(props.authors as Array<keyof typeof AUTHORS>).map((author) => (
<Avatar
key={author}
alt=""
src={`${AUTHORS[author].avatar}?s=${28}`}
srcSet={`${AUTHORS[author].avatar}?s=${28 * 2} 2x, ${AUTHORS[author].avatar}?s=${
28 * 3
} 3x`}
/>
))}
</AvatarGroup>
)}
<Box
sx={{
display: { sm: 'block', md: 'flex' },
justifyContent: 'space-between',
alignItems: 'end',
}}
>
<Box sx={{ position: 'relative' }}>
{props.authors && (
<Typography variant="body2" sx={{ fontWeight: 'medium' }}>
{props.authors
.slice(0, 3)
.map((userId) => {
const name = AUTHORS[userId as keyof typeof AUTHORS]?.name;
if (name) {
if (props.authors && props.authors.length > 1) {
// display only firstName
return name.split(' ')[0];
}
return name;
}
return userId;
})
.join(', ')}
{props.authors.length > 2 && ', and more.'}
</Typography>
)}
{props.date && (
<Typography variant="caption" sx={{ fontWeight: 'regular', color: 'text.tertiary' }}>
{new Date(props.date).toDateString()}
</Typography>
)}
</Box>
<Button
component={Link}
aria-describedby={`describe-${props.slug}`}
href={`/blog/${props.slug}`}
id={`describe-${props.slug}`}
endIcon={<KeyboardArrowRightRoundedIcon />}
size="small"
sx={{ mt: { xs: 0.5, md: 0 }, p: { xs: 0, sm: '6px 8px' } }}
>
Read more
</Button>
</Box>
</React.Fragment>
);
}
const PAGE_SIZE = 7;
export default function Blog(props: InferGetStaticPropsType<typeof getStaticProps>) {
const router = useRouter();
const postListRef = React.useRef<HTMLDivElement>(null);
const [page, setPage] = React.useState(0);
const [selectedTags, setSelectedTags] = React.useState<Record<string, boolean>>({});
const { allBlogPosts, tagInfo: rawTagInfo } = props;
const [firstPost, secondPost, ...otherPosts] = allBlogPosts;
const tagInfo = { ...rawTagInfo };
[firstPost, secondPost].forEach((post) => {
post.tags.forEach((tag) => {
if (tagInfo[tag]) {
tagInfo[tag]! -= 1;
}
});
});
Object.entries(tagInfo).forEach(([tagName, tagCount]) => {
if (tagCount === 0) {
delete tagInfo[tagName];
}
});
const filteredPosts = otherPosts.filter((post) => {
if (Object.keys(selectedTags).length === 0) {
return true;
}
return post.tags.some((tag) => {
return Object.keys(selectedTags).includes(tag);
});
});
const pageStart = page * PAGE_SIZE;
const totalPage = Math.ceil(filteredPosts.length / PAGE_SIZE);
const displayedPosts = filteredPosts.slice(pageStart, pageStart + PAGE_SIZE);
const getTags = React.useCallback(() => {
const { tags = '' } = router.query;
return (typeof tags === 'string' ? tags.split(',') : tags || [])
.map((str) => str.trim())
.filter((tag) => !!tag);
}, [router.query]);
React.useEffect(() => {
const arrayTags = getTags();
const finalTags: Record<string, boolean> = {};
arrayTags.forEach((tag) => {
finalTags[tag] = true;
});
setSelectedTags(finalTags);
setPage(0);
}, [getTags]);
const removeTag = (tag: string) => {
router.push(
{
query: {
...router.query,
tags: getTags().filter((value) => value !== tag),
},
},
undefined,
{ shallow: true },
);
};
return (
<BrandingCssVarsProvider>
<Head
title="Blog - MUI"
description="Follow the MUI blog to learn about new product features, latest advancements in UI development, and business initiatives."
card="/static/social-previews/blog-preview.jpg"
disableAlternateLocale
/>
<AppHeader />
<main id="main-content">
<Section cozy bg="gradient">
<SectionHeadline
alwaysCenter
overline="Blog"
title={
<Typography variant="h2" component="h1">
Stay <GradientText>in the loop</GradientText> with
<br /> the latest about MUI&apos;s products
</Typography>
}
/>
</Section>
<Divider />
<Container sx={{ mt: { xs: 2, sm: -6 } }}>
<Box
component="ul"
sx={{
display: 'grid',
m: 0,
p: 0,
gap: 2,
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
}}
>
{[firstPost, secondPost].map((post) => (
<Paper
key={post.slug}
component="li"
variant="outlined"
sx={(theme) => ({
p: 2,
display: 'flex',
flexDirection: 'column',
backgroundImage: (theme.vars || theme).palette.gradients.radioSubtle,
boxShadow: '0 4px 12px rgba(170, 180, 190, 0.2)',
...theme.applyDarkStyles({
background: (theme.vars || theme).palette.primaryDark[900],
backgroundImage: (theme.vars || theme).palette.gradients.radioSubtle,
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.4)',
}),
})}
>
{post.image && (
<Box
component="img"
src={post.image}
sx={{
aspectRatio: '16 / 9',
width: '100%',
height: 'auto',
objectFit: 'cover',
borderRadius: '4px',
}}
/>
)}
<PostPreview {...post} />
</Paper>
))}
</Box>
</Container>
<Container
ref={postListRef}
sx={{
py: { xs: 4, sm: 6, md: 8 },
mt: -6,
display: 'grid',
gridTemplateColumns: { md: '1fr 380px' },
columnGap: 8,
}}
>
<Typography
component="h2"
variant="h6"
sx={{ fontWeight: 'semiBold', mb: { xs: 1, sm: 2 }, mt: 8 }}
>
Posts{' '}
{Object.keys(selectedTags).length ? (
<span>
tagged as{' '}
<Typography component="span" variant="inherit" color="primary" noWrap>
&quot;{Object.keys(selectedTags)[0]}&quot;
</Typography>
</span>
) : (
''
)}
</Typography>
<Box sx={{ gridRow: 'span 2' }}>
<Box
sx={{
position: 'sticky',
top: 90,
mt: { xs: 0, md: 9 },
mb: { xs: 2, md: 0 },
display: 'flex',
flexDirection: 'column',
gap: 2,
'& .MuiPaper-root': {
p: 2,
bgcolor: 'transparent',
borderColor: 'divider',
},
}}
>
<Paper variant="outlined">
<Typography
component="h3"
variant="subtitle2"
sx={{ color: 'text.primary', fontWeight: 'semiBold', mb: 2 }}
>
Filter posts by tag
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{Object.keys(tagInfo).map((tag) => {
const selected = !!selectedTags[tag];
return (
<Chip
key={tag}
variant={selected ? 'filled' : 'outlined'}
color={selected ? 'primary' : undefined}
{...(selected
? {
label: tag,
onDelete: () => {
postListRef.current?.scrollIntoView();
removeTag(tag);
},
}
: {
label: tag,
onClick: () => {
postListRef.current?.scrollIntoView();
router.push(
{
query: {
...router.query,
tags: tag,
},
},
undefined,
{ shallow: true },
);
},
})}
size="small"
/>
);
})}
</Box>
</Paper>
<Paper variant="outlined">
<Typography
component="h3"
variant="subtitle2"
gutterBottom
sx={{ color: 'text.primary', fontWeight: 'semiBold' }}
>
Want to hear more from us?
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 2 }}>
Get up to date with everything MUI-related through our social media:
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, '* > svg': { mr: 1 } }}>
<Link href="https://github.com/mui" target="_blank" sx={{ fontSize: 14 }}>
<GitHubIcon fontSize="small" />
GitHub
</Link>
<Link href="https://x.com/MUI_hq" target="_blank" sx={{ fontSize: 14 }}>
<XIcon fontSize="small" />X
</Link>
<Link href="https://mui.com/r/discord/" target="_blank" sx={{ fontSize: 14 }}>
<DiscordIcon fontSize="small" />
Discord
</Link>
<Link
href="https://www.linkedin.com/company/mui/"
target="_blank"
sx={{ fontSize: 14 }}
>
<LinkedInIcon fontSize="small" />
LinkedIn
</Link>
<Link
href="https://www.youtube.com/@MUI_hq"
target="_blank"
sx={{ fontSize: 14 }}
>
<YouTubeIcon fontSize="small" />
Youtube
</Link>
</Box>
</Paper>
</Box>
</Box>
<div>
<Box component="ul" sx={{ p: 0, m: 0 }}>
{displayedPosts.map((post) => (
<Box
component="li"
key={post.slug}
sx={{
py: 2.5,
display: 'flex',
flexDirection: 'column',
position: 'relative',
'&:not(:last-of-type)': {
borderBottom: '1px solid',
borderColor: 'divider',
},
}}
>
<PostPreview {...post} />
</Box>
))}
</Box>
<Pagination
page={page + 1}
count={totalPage}
variant="outlined"
shape="rounded"
onChange={(_, value) => {
setPage(value - 1);
postListRef.current?.scrollIntoView();
}}
sx={{ mt: 1, mb: 4 }}
/>
</div>
</Container>
</main>
<Divider />
<HeroEnd />
<Divider />
<AppFooter />
</BrandingCssVarsProvider>
);
}