✨ Post list
This commit is contained in:
		| @@ -2,24 +2,25 @@ import { SnAttachment } from '@/services/attachment' | |||||||
| import { getAttachmentUrl } from '@/services/network' | import { getAttachmentUrl } from '@/services/network' | ||||||
| import { QuestionMark } from '@mui/icons-material' | import { QuestionMark } from '@mui/icons-material' | ||||||
| import { Link, Paper, Typography } from '@mui/material' | import { Link, Paper, Typography } from '@mui/material' | ||||||
|  | import { ComponentProps } from 'react' | ||||||
|  |  | ||||||
| export function AttachmentItem({ item }: { item: SnAttachment }) { | export function AttachmentItem({ item, ...rest }: { item: SnAttachment } & ComponentProps<'div'>) { | ||||||
|   switch (item.mimetype.split('/')[0]) { |   switch (item.mimetype.split('/')[0]) { | ||||||
|     case 'image': |     case 'image': | ||||||
|       return ( |       return ( | ||||||
|         <Paper> |         <Paper {...rest}> | ||||||
|           <img src={getAttachmentUrl(item.rid)} alt={item.alt} style={{ objectFit: 'cover', borderRadius: '8px' }} /> |           <img src={getAttachmentUrl(item.rid)} alt={item.alt} style={{ objectFit: 'cover', borderRadius: '8px' }} /> | ||||||
|         </Paper> |         </Paper> | ||||||
|       ) |       ) | ||||||
|     case 'video': |     case 'video': | ||||||
|       return ( |       return ( | ||||||
|         <Paper> |         <Paper {...rest}> | ||||||
|           <video src={getAttachmentUrl(item.rid)} controls style={{ borderRadius: '8px' }} /> |           <video src={getAttachmentUrl(item.rid)} controls style={{ borderRadius: '8px' }} /> | ||||||
|         </Paper> |         </Paper> | ||||||
|       ) |       ) | ||||||
|     default: |     default: | ||||||
|       return ( |       return ( | ||||||
|         <Paper sx={{ width: '100%', height: '100%', p: 5, textAlign: 'center' }}> |         <Paper sx={{ width: '100%', height: '100%', p: 5, textAlign: 'center' }} {...rest}> | ||||||
|           <QuestionMark sx={{ mb: 2 }} /> |           <QuestionMark sx={{ mb: 2 }} /> | ||||||
|           <Typography>Unknown</Typography> |           <Typography>Unknown</Typography> | ||||||
|           <Typography gutterBottom>{item.name}</Typography> |           <Typography gutterBottom>{item.name}</Typography> | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ export default function App({ Component, pageProps }: AppProps) { | |||||||
|         <CssBaseline /> |         <CssBaseline /> | ||||||
|  |  | ||||||
|         <CapAppBar /> |         <CapAppBar /> | ||||||
|         <Box sx={{ height: 'calc(100vh - 64px)' }}> |         <Box sx={{ minHeight: 'calc(100vh - 64px)' }}> | ||||||
|           <Component {...pageProps} /> |           <Component {...pageProps} /> | ||||||
|         </Box> |         </Box> | ||||||
|       </ThemeProvider> |       </ThemeProvider> | ||||||
|   | |||||||
| @@ -83,7 +83,7 @@ export default function Login() { | |||||||
|       sx={{ |       sx={{ | ||||||
|         display: 'grid', |         display: 'grid', | ||||||
|         placeItems: 'center', |         placeItems: 'center', | ||||||
|         height: '100%', |         height: 'calc(100vh - 64px)', | ||||||
|         textAlign: 'center', |         textAlign: 'center', | ||||||
|       }} |       }} | ||||||
|     > |     > | ||||||
|   | |||||||
| @@ -216,7 +216,7 @@ export default function Post({ post, attachments }: InferGetServerSidePropsType< | |||||||
|             }} |             }} | ||||||
|           > |           > | ||||||
|             {attachments.map((a) => ( |             {attachments.map((a) => ( | ||||||
|               <Grid size="grow"> |               <Grid size={1} key={a.id}> | ||||||
|                 <AttachmentItem item={a} /> |                 <AttachmentItem item={a} /> | ||||||
|               </Grid> |               </Grid> | ||||||
|             ))} |             ))} | ||||||
|   | |||||||
							
								
								
									
										135
									
								
								src/pages/posts/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/pages/posts/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | |||||||
|  | import { AttachmentItem } from '@/components/attachments/AttachmentItem' | ||||||
|  | import { SnAttachment, listAttachment } from '@/services/attachment' | ||||||
|  | import { getAttachmentUrl, sni } from '@/services/network' | ||||||
|  | import { SnPost } from '@/services/post' | ||||||
|  | import { Avatar, Box, Container, Divider, Grid2 as Grid, Pagination, Paper, Typography } from '@mui/material' | ||||||
|  | import { GetServerSideProps, InferGetServerSidePropsType } from 'next' | ||||||
|  | import Link from 'next/link' | ||||||
|  | import { useRouter } from 'next/router' | ||||||
|  | import rehypeSanitize from 'rehype-sanitize' | ||||||
|  | import rehypeStringify from 'rehype-stringify' | ||||||
|  | import remarkBreaks from 'remark-breaks' | ||||||
|  | import remarkParse from 'remark-parse' | ||||||
|  | import remarkRehype from 'remark-rehype' | ||||||
|  | import { unified } from 'unified' | ||||||
|  |  | ||||||
|  | type SnPostWithAttachments = SnPost & { attachments: SnAttachment[] } | ||||||
|  |  | ||||||
|  | export const getServerSideProps = (async (context) => { | ||||||
|  |   let page: number = parseInt(context.query.page as string) | ||||||
|  |   if (isNaN(page)) page = 1 | ||||||
|  |  | ||||||
|  |   const countPerPage = 10 | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     const { data: resp } = await sni.get<{ data: SnPost[]; count: number }>('/cgi/co/posts', { | ||||||
|  |       params: { | ||||||
|  |         take: countPerPage, | ||||||
|  |         offset: (page - 1) * countPerPage, | ||||||
|  |       }, | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     let posts: SnPostWithAttachments[] = resp.data as SnPostWithAttachments[] | ||||||
|  |     for (let idx = 0; idx < posts.length; idx++) { | ||||||
|  |       let post = posts[idx] | ||||||
|  |       if (post.body.content) { | ||||||
|  |         let processor: any = unified().use(remarkParse) | ||||||
|  |         if (post.type != 'article') { | ||||||
|  |           processor = processor.use(remarkBreaks) | ||||||
|  |         } | ||||||
|  |         const out = await processor | ||||||
|  |           .use(remarkRehype) | ||||||
|  |           .use(rehypeSanitize) | ||||||
|  |           .use(rehypeStringify) | ||||||
|  |           .process(post.body.content) | ||||||
|  |         post.body.rawContent = post.body.content | ||||||
|  |         post.body.content = String(out) | ||||||
|  |       } | ||||||
|  |       if (post.body.attachments) { | ||||||
|  |         post.attachments = await listAttachment(post.body.attachments) | ||||||
|  |       } | ||||||
|  |       posts[idx] = post | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return { props: { posts, page, pages: Math.ceil(resp.count / countPerPage) } } | ||||||
|  |   } catch (err) { | ||||||
|  |     console.error(err) | ||||||
|  |     return { | ||||||
|  |       notFound: true, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }) satisfies GetServerSideProps<{ posts: SnPostWithAttachments[]; page: number; pages: number }> | ||||||
|  |  | ||||||
|  | export default function PostList({ posts, page, pages }: InferGetServerSidePropsType<typeof getServerSideProps>) { | ||||||
|  |   const router = useRouter() | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Container sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 2 }} maxWidth="md"> | ||||||
|  |       {posts.map((p) => ( | ||||||
|  |         <Paper key={p.id} sx={{ px: 2, py: 2 }}> | ||||||
|  |           <Box sx={{ display: 'flex', justifyContent: 'space-between' }}> | ||||||
|  |             <Box sx={{ display: 'flex', gap: 2 }}> | ||||||
|  |               <Avatar src={getAttachmentUrl(p.publisher.avatar)} /> | ||||||
|  |               <Box sx={{ display: 'flex', flexDirection: 'column' }}> | ||||||
|  |                 <Typography fontWeight="bold">{p.publisher.nick}</Typography> | ||||||
|  |                 <Typography fontFamily="monospace" fontSize={13} lineHeight={1.2}> | ||||||
|  |                   @{p.publisher.name} | ||||||
|  |                 </Typography> | ||||||
|  |               </Box> | ||||||
|  |             </Box> | ||||||
|  |           </Box> | ||||||
|  |  | ||||||
|  |           <Link href={`/posts/${p.id}`} passHref> | ||||||
|  |             <Box> | ||||||
|  |               <Box sx={{ mt: 1.5, mb: 1 }} display="flex" flexDirection="column" gap={0.5}> | ||||||
|  |                 {(p.body.title || p.body.content) && ( | ||||||
|  |                   <Box> | ||||||
|  |                     {p.body.title && <Typography variant="h6">{p.body.title}</Typography>} | ||||||
|  |                     {p.body.description && <Typography variant="subtitle1">{p.body.description}</Typography>} | ||||||
|  |                   </Box> | ||||||
|  |                 )} | ||||||
|  |                 <Box display="flex" gap={2} sx={{ opacity: 0.8 }}> | ||||||
|  |                   <Typography variant="body2"> | ||||||
|  |                     Published at {new Date(p.publishedAt ?? p.createdAt).toLocaleString()} | ||||||
|  |                   </Typography> | ||||||
|  |                 </Box> | ||||||
|  |               </Box> | ||||||
|  |  | ||||||
|  |               <Divider /> | ||||||
|  |  | ||||||
|  |               <Box sx={{ maxWidth: 'unset' }} className="prose prose-md"> | ||||||
|  |                 {p.body.content && <div dangerouslySetInnerHTML={{ __html: p.body.content }} />} | ||||||
|  |               </Box> | ||||||
|  |             </Box> | ||||||
|  |           </Link> | ||||||
|  |  | ||||||
|  |           {p.attachments && ( | ||||||
|  |             <Grid | ||||||
|  |               container | ||||||
|  |               spacing={2} | ||||||
|  |               columns={{ | ||||||
|  |                 xs: 1, | ||||||
|  |                 sm: Math.min(2, p.attachments.length), | ||||||
|  |                 md: Math.min(3, p.attachments.length), | ||||||
|  |                 lg: Math.min(4, p.attachments.length), | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               {p.attachments.map((a) => ( | ||||||
|  |                 <Grid size={1} key={a.id}> | ||||||
|  |                   <AttachmentItem item={a} /> | ||||||
|  |                 </Grid> | ||||||
|  |               ))} | ||||||
|  |             </Grid> | ||||||
|  |           )} | ||||||
|  |         </Paper> | ||||||
|  |       ))} | ||||||
|  |  | ||||||
|  |       <Pagination | ||||||
|  |         count={pages} | ||||||
|  |         page={page} | ||||||
|  |         sx={{ mx: 'auto', mb: 5, mt: 3 }} | ||||||
|  |         onChange={(_, page) => router.push('/posts?page=' + page)} | ||||||
|  |       /> | ||||||
|  |     </Container> | ||||||
|  |   ) | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user