✨ App drawer
💄 Support breaks for story posts
This commit is contained in:
parent
b4c5361f28
commit
8cbdc7c870
@ -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',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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,38 +52,63 @@ export function CapAppBar() {
|
|||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
return (
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
||||||
<ElevationScroll elevation={2} color="white">
|
|
||||||
<AppBar position="sticky" elevation={0} color="transparent">
|
|
||||||
<Toolbar>
|
|
||||||
<IconButton size="large" edge="start" color="inherit" aria-label="menu" sx={{ mr: 2 }}>
|
|
||||||
<MenuIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Link href="/" passHref style={{ flexGrow: 1 }}>
|
|
||||||
<Typography variant="h6" component="div">
|
|
||||||
Capital
|
|
||||||
</Typography>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link href={userStore.account ? '/users/me' : '/auth/login'} passHref>
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const drawerWidth = 280
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="large"
|
size="large"
|
||||||
aria-label="account of current user"
|
edge="start"
|
||||||
aria-controls="primary-search-account-menu"
|
|
||||||
aria-haspopup="true"
|
|
||||||
color="inherit"
|
color="inherit"
|
||||||
|
aria-label="menu"
|
||||||
|
sx={{ mr: 2 }}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
{userStore.account ? (
|
<MenuIcon />
|
||||||
<Avatar sx={{ backgroundColor: 'transparent' }} src={getAttachmentUrl(userStore.account.avatar)} />
|
|
||||||
) : (
|
|
||||||
<Avatar sx={{ backgroundColor: 'transparent' }}>
|
|
||||||
<AccountCircle sx={{ color: theme.palette.text.primary }} />
|
|
||||||
</Avatar>
|
|
||||||
)}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Link>
|
<Link href="/" passHref style={{ flexGrow: 1 }}>
|
||||||
</Toolbar>
|
<Typography variant="h6" component="div">
|
||||||
</AppBar>
|
Capital
|
||||||
</ElevationScroll>
|
</Typography>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href={userStore.account ? '/users/me' : '/auth/login'} passHref>
|
||||||
|
<IconButton
|
||||||
|
size="large"
|
||||||
|
aria-label="account of current user"
|
||||||
|
aria-controls="primary-search-account-menu"
|
||||||
|
aria-haspopup="true"
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
{userStore.account ? (
|
||||||
|
<Avatar sx={{ backgroundColor: 'transparent' }} src={getAttachmentUrl(userStore.account.avatar)} />
|
||||||
|
) : (
|
||||||
|
<Avatar sx={{ backgroundColor: 'transparent' }}>
|
||||||
|
<AccountCircle sx={{ color: theme.palette.text.primary }} />
|
||||||
|
</Avatar>
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
</Link>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
</ElevationScroll>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
87
src/components/CapDrawer.tsx
Normal file
87
src/components/CapDrawer.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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) => (
|
||||||
<AttachmentItem item={a} />
|
<Grid size="grow">
|
||||||
|
<AttachmentItem item={a} />
|
||||||
|
</Grid>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
Loading…
Reference in New Issue
Block a user