App drawer

💄 Support breaks for story posts
This commit is contained in:
LittleSheep 2025-01-04 12:48:34 +08:00
parent b4c5361f28
commit 8cbdc7c870
6 changed files with 183 additions and 45 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -4,7 +4,16 @@ const nextConfig: NextConfig = {
/* config options here */ /* config options here */
reactStrictMode: true, reactStrictMode: true,
images: { images: {
domains: ['raw.sn.solsynth.dev', 'api.sn.solsynth.dev'], remotePatterns: [
{
protocol: 'https',
hostname: 'raw.sn.solsynth.dev',
},
{
protocol: 'https',
hostname: 'api.sn.solsynth.dev',
},
],
}, },
} }

View File

@ -27,6 +27,7 @@
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"rehype-sanitize": "^6.0.0", "rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1", "rehype-stringify": "^10.0.1",
"remark-breaks": "^4.0.0",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1", "remark-rehype": "^11.1.1",
"unified": "^11.0.5", "unified": "^11.0.5",

View File

@ -3,11 +3,10 @@ import {
AppBar, AppBar,
AppBarProps, AppBarProps,
Avatar, Avatar,
createTheme,
IconButton, IconButton,
ThemeProvider,
Toolbar, Toolbar,
Typography, Typography,
useMediaQuery,
useScrollTrigger, useScrollTrigger,
useTheme, useTheme,
} from '@mui/material' } from '@mui/material'
@ -15,11 +14,13 @@ import { getAttachmentUrl } from '@/services/network'
import MenuIcon from '@mui/icons-material/Menu' import MenuIcon from '@mui/icons-material/Menu'
import AccountCircle from '@mui/icons-material/AccountCircle' import AccountCircle from '@mui/icons-material/AccountCircle'
import Link from 'next/link' import Link from 'next/link'
import React from 'react' import React, { useState } from 'react'
import { CapDrawer } from './CapDrawer'
interface ElevationAppBarProps { interface ElevationAppBarProps {
elevation?: number elevation?: number
color?: any color?: any
isMobile: boolean
children?: React.ReactElement<{ elevation?: number } & AppBarProps> children?: React.ReactElement<{ elevation?: number } & AppBarProps>
} }
@ -51,11 +52,35 @@ export function CapAppBar() {
const userStore = useUserStore() const userStore = useUserStore()
const theme = useTheme() const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const [open, setOpen] = useState(false)
const drawerWidth = 280
return ( return (
<ElevationScroll elevation={2} color="white"> <>
<AppBar position="sticky" elevation={0} color="transparent"> <CapDrawer width={drawerWidth} open={open} onClose={() => setOpen(false)} />
<ElevationScroll isMobile={isMobile} elevation={2} color="white">
<AppBar
position="sticky"
elevation={0}
color="transparent"
sx={{
width: isMobile ? null : `calc(100% - ${drawerWidth}`,
marginLeft: isMobile ? null : `${drawerWidth}px`,
}}
>
<Toolbar> <Toolbar>
<IconButton size="large" edge="start" color="inherit" aria-label="menu" sx={{ mr: 2 }}> <IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }}
onClick={() => setOpen(true)}
>
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<Link href="/" passHref style={{ flexGrow: 1 }}> <Link href="/" passHref style={{ flexGrow: 1 }}>
@ -84,5 +109,6 @@ export function CapAppBar() {
</Toolbar> </Toolbar>
</AppBar> </AppBar>
</ElevationScroll> </ElevationScroll>
</>
) )
} }

View File

@ -0,0 +1,87 @@
import {
Box,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Divider,
Drawer,
Toolbar,
} from '@mui/material'
import { JSX } from 'react'
import FeedIcon from '@mui/icons-material/Feed'
import PhotoLibraryIcon from '@mui/icons-material/PhotoLibrary'
import InfoIcon from '@mui/icons-material/Info'
import PolicyIcon from '@mui/icons-material/Policy'
import Link from 'next/link'
interface NavLink {
title: string
icon: JSX.Element
href: string
}
export function CapDrawer({ width, open, onClose }: { width: number; open: boolean; onClose: () => void }) {
const functionLinks: NavLink[] = [
{
title: 'Posts',
icon: <FeedIcon />,
href: '/posts',
},
{
title: 'Gallery',
icon: <PhotoLibraryIcon />,
href: '/attachments',
},
]
const additionalLinks: NavLink[] = [
{
title: 'About',
icon: <InfoIcon />,
href: '/about',
},
{
title: 'Term & Conditions',
icon: <PolicyIcon />,
href: '/terms',
},
]
return (
<Drawer open={open} onClose={onClose}>
<Box sx={{ width: width }} role="presentation" onClick={onClose}>
<Toolbar>Solsynth LLC</Toolbar>
<Divider />
<List>
{functionLinks.map((l) => (
<Link passHref href={l.href} key={l.href}>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>{l.icon}</ListItemIcon>
<ListItemText primary={l.title} />
</ListItemButton>
</ListItem>
</Link>
))}
</List>
<Divider />
<List dense>
{additionalLinks.map((l) => (
<Link passHref href={l.href} key={l.href}>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>{l.icon}</ListItemIcon>
<ListItemText primary={l.title} />
</ListItemButton>
</ListItem>
</Link>
))}
</List>
</Box>
</Drawer>
)
}

View File

@ -22,6 +22,7 @@ import Head from 'next/head'
import Image from 'next/image' import Image from 'next/image'
import rehypeSanitize from 'rehype-sanitize' import rehypeSanitize from 'rehype-sanitize'
import rehypeStringify from 'rehype-stringify' import rehypeStringify from 'rehype-stringify'
import remarkBreaks from 'remark-breaks'
import remarkParse from 'remark-parse' import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype' import remarkRehype from 'remark-rehype'
@ -32,15 +33,12 @@ export const getServerSideProps = (async (context) => {
try { try {
const { data: post } = await sni.get<SnPost>('/cgi/co/posts/' + id.join(':')) const { data: post } = await sni.get<SnPost>('/cgi/co/posts/' + id.join(':'))
if (post.body.content) { if (post.body.content) {
if (!post.body.description) { let processor: any = unified().use(remarkParse)
post.body.description = post.body.content.replaceAll('\n', ' ').substring(0, 200) if (post.type != 'article') {
processor = processor.use(remarkBreaks)
} }
const out = await unified() const out = await processor.use(remarkRehype).use(rehypeSanitize).use(rehypeStringify).process(post.body.content)
.use(remarkParse) post.body.rawContent = post.body.content
.use(remarkRehype)
.use(rehypeSanitize)
.use(rehypeStringify)
.process(post.body.content)
post.body.content = String(out) post.body.content = String(out)
} }
let attachments: SnAttachment[] = [] let attachments: SnAttachment[] = []
@ -49,6 +47,7 @@ export const getServerSideProps = (async (context) => {
} }
return { props: { post, attachments } } return { props: { post, attachments } }
} catch (err) { } catch (err) {
console.error(err)
return { return {
notFound: true, notFound: true,
} }
@ -72,7 +71,11 @@ export default function Post({ post, attachments }: InferGetServerSidePropsType<
: `Post #${post.id} / @${post.publisher.name} / Solar Network`, : `Post #${post.id} / @${post.publisher.name} / Solar Network`,
[post], [post],
) )
const description = useMemo(() => post.body.description, [post]) const description = useMemo(
() =>
post.body.description ? post.body.description : post.body.rawContent.replaceAll('\n', ' ').substring(0, 200),
[post],
)
const image = useMemo(() => { const image = useMemo(() => {
if (post.body.thumbnail) { if (post.body.thumbnail) {
@ -80,21 +83,21 @@ export default function Post({ post, attachments }: InferGetServerSidePropsType<
} }
if (attachments) { if (attachments) {
const images = attachments.filter((a) => a.mimetype.startsWith('image')) const images = attachments.filter((a) => a.mimetype.startsWith('image'))
if (images) return getAttachmentUrl(images[0].rid) if (images && images[0]) return getAttachmentUrl(images[0].rid)
} }
return null return null
}, [post]) }, [post])
const video = useMemo(() => { const video = useMemo(() => {
if (attachments) { if (attachments) {
const videos = attachments.filter((a) => a.mimetype.startsWith('video')) const videos = attachments.filter((a) => a.mimetype.startsWith('video'))
if (videos) return getAttachmentUrl(videos[0].rid) if (videos && videos[0]) return getAttachmentUrl(videos[0].rid)
} }
return null return null
}, [post]) }, [post])
const audio = useMemo(() => { const audio = useMemo(() => {
if (attachments) { if (attachments) {
const audios = attachments.filter((a) => a.mimetype.startsWith('audio')) const audios = attachments.filter((a) => a.mimetype.startsWith('audio'))
if (audios) return getAttachmentUrl(audios[0].rid) if (audios && audios[0]) return getAttachmentUrl(audios[0].rid)
} }
return null return null
}, [post]) }, [post])
@ -201,9 +204,21 @@ export default function Post({ post, attachments }: InferGetServerSidePropsType<
</Box> </Box>
{attachments && ( {attachments && (
<Grid container spacing={2} sx={{ mt: 3 }} columns={{ md: Math.min(2, attachments.length) }}> <Grid
container
spacing={2}
sx={{ mt: 3 }}
columns={{
xs: 1,
sm: Math.min(2, attachments.length),
md: Math.min(3, attachments.length),
lg: Math.min(4, attachments.length),
}}
>
{attachments.map((a) => ( {attachments.map((a) => (
<Grid size="grow">
<AttachmentItem item={a} /> <AttachmentItem item={a} />
</Grid>
))} ))}
</Grid> </Grid>
)} )}