✨ Basic post preview
This commit is contained in:
parent
a56f098413
commit
02ccbdac71
@ -17,6 +17,7 @@
|
|||||||
"@mui/material": "^6.3.0",
|
"@mui/material": "^6.3.0",
|
||||||
"@mui/material-nextjs": "^6.3.1",
|
"@mui/material-nextjs": "^6.3.1",
|
||||||
"@mui/x-charts": "^7.23.2",
|
"@mui/x-charts": "^7.23.2",
|
||||||
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"axios-case-converter": "^1.1.1",
|
"axios-case-converter": "^1.1.1",
|
||||||
"cookies-next": "^5.0.2",
|
"cookies-next": "^5.0.2",
|
||||||
@ -24,6 +25,11 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
|
"rehype-sanitize": "^6.0.0",
|
||||||
|
"rehype-stringify": "^10.0.1",
|
||||||
|
"remark-parse": "^11.0.0",
|
||||||
|
"remark-rehype": "^11.1.1",
|
||||||
|
"unified": "^11.0.5",
|
||||||
"zustand": "^5.0.2"
|
"zustand": "^5.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -41,7 +41,7 @@ function ElevationScroll(props: ElevationAppBarProps) {
|
|||||||
? React.cloneElement(props.children, {
|
? React.cloneElement(props.children, {
|
||||||
elevation: trigger ? props.elevation : 0,
|
elevation: trigger ? props.elevation : 0,
|
||||||
sx: trigger
|
sx: trigger
|
||||||
? { backgroundColor: props.color, ...commonStyle }
|
? { backgroundColor: props.color, color: 'black', ...commonStyle }
|
||||||
: { backgroundColor: 'transparent', ...commonStyle },
|
: { backgroundColor: 'transparent', ...commonStyle },
|
||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
|
106
src/pages/posts/[...id].tsx
Normal file
106
src/pages/posts/[...id].tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { getAttachmentUrl, sni } from '@/services/network'
|
||||||
|
import { SnPost } from '@/services/post'
|
||||||
|
import { Alert, AlertTitle, Avatar, Box, Collapse, Container, IconButton, Link, Typography } from '@mui/material'
|
||||||
|
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { unified } from 'unified'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import rehypeSanitize from 'rehype-sanitize'
|
||||||
|
import rehypeStringify from 'rehype-stringify'
|
||||||
|
import remarkParse from 'remark-parse'
|
||||||
|
import remarkRehype from 'remark-rehype'
|
||||||
|
|
||||||
|
import CloseIcon from '@mui/icons-material/Close'
|
||||||
|
|
||||||
|
export const getServerSideProps = (async (context) => {
|
||||||
|
const id = context.params!.id as string[]
|
||||||
|
try {
|
||||||
|
const { data: post } = await sni.get<SnPost>('/cgi/co/posts/' + id.join(':'))
|
||||||
|
if (post.body.content) {
|
||||||
|
const out = await unified()
|
||||||
|
.use(remarkParse)
|
||||||
|
.use(remarkRehype)
|
||||||
|
.use(rehypeSanitize)
|
||||||
|
.use(rehypeStringify)
|
||||||
|
.process(post.body.content)
|
||||||
|
post.body.content = String(out)
|
||||||
|
}
|
||||||
|
return { props: { post } }
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) satisfies GetServerSideProps<{ post: SnPost }>
|
||||||
|
|
||||||
|
export default function Post({ post }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||||
|
const link = useMemo(() => `https://sn.solsynth.dev/posts/${post.id}`, [post])
|
||||||
|
|
||||||
|
const [openAppHint, setOpenAppHint] = useState<boolean>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!localStorage.getItem('sol_hide_app_hint')) {
|
||||||
|
setOpenAppHint(true)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (openAppHint === false) {
|
||||||
|
localStorage.setItem('sol_hide_app_hint', 'yes')
|
||||||
|
}
|
||||||
|
}, [openAppHint])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Collapse in={openAppHint}>
|
||||||
|
<Alert
|
||||||
|
variant="filled"
|
||||||
|
severity="info"
|
||||||
|
sx={{ borderRadius: 0, px: 3 }}
|
||||||
|
action={
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setOpenAppHint(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon fontSize="inherit" />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AlertTitle gutterBottom={false}>Open in Solian</AlertTitle>
|
||||||
|
All feature supported, cross-platform, the official app of Solar Network.{' '}
|
||||||
|
<Link href={link} color="#ffffff">
|
||||||
|
Launch
|
||||||
|
</Link>
|
||||||
|
</Alert>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
{post.body.thumbnail && (
|
||||||
|
<Box sx={{ aspectRatio: 16 / 9, position: 'relative', borderBottom: 1, borderTop: 1, borderColor: 'divider' }}>
|
||||||
|
<Image src={getAttachmentUrl(post.body.thumbnail)} alt="post thumbnail" fill />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Container sx={{ mt: 3, pb: 5 }} maxWidth="md">
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
|
<Avatar src={getAttachmentUrl(post.publisher.avatar)} />
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Typography fontWeight="bold">{post.publisher.nick}</Typography>
|
||||||
|
<Typography fontFamily="monospace" fontSize={13} lineHeight={1.2}>
|
||||||
|
@{post.publisher.name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 2.5, maxWidth: 'unset' }} component="article" className="prose prose-lg">
|
||||||
|
{post.body.content && <div dangerouslySetInnerHTML={{ __html: post.body.content }} />}
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
48
src/services/attachment.ts
Normal file
48
src/services/attachment.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { sni } from './network'
|
||||||
|
|
||||||
|
export interface SnAttachment {
|
||||||
|
id: number
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
deletedAt?: Date | null
|
||||||
|
rid: string
|
||||||
|
uuid: string
|
||||||
|
size: number
|
||||||
|
name: string
|
||||||
|
alt: string
|
||||||
|
mimetype: string
|
||||||
|
hash: string
|
||||||
|
destination: number
|
||||||
|
refCount: number
|
||||||
|
contentRating: number
|
||||||
|
qualityRating: number
|
||||||
|
cleanedAt?: Date | null
|
||||||
|
isAnalyzed: boolean
|
||||||
|
isSelfRef: boolean
|
||||||
|
isIndexable: boolean
|
||||||
|
ref?: SnAttachment | null
|
||||||
|
refId?: number | null
|
||||||
|
poolId?: number | null
|
||||||
|
accountId: number
|
||||||
|
thumbnailId?: number | null
|
||||||
|
thumbnail?: SnAttachment | null
|
||||||
|
compressedId?: number | null
|
||||||
|
compressed?: SnAttachment | null
|
||||||
|
usermeta: Record<string, any>
|
||||||
|
metadata: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAttachment(id: string | number): Promise<SnAttachment> {
|
||||||
|
const resp = await sni.get<SnAttachment>('/cgi/uc/attachments/' + id + '/meta')
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listAttachment(id: string[]): Promise<SnAttachment[]> {
|
||||||
|
const resp = await sni.get<{ data: SnAttachment[] }>('/cgi/uc/attachments', {
|
||||||
|
params: {
|
||||||
|
id: id.join(','),
|
||||||
|
take: id.length,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return resp.data.data
|
||||||
|
}
|
85
src/services/post.ts
Normal file
85
src/services/post.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
export interface SnPost {
|
||||||
|
id: number
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
deletedAt?: Date | null
|
||||||
|
type: string
|
||||||
|
body: SnPostBody & Record<string, any>
|
||||||
|
language: string
|
||||||
|
alias?: string | null
|
||||||
|
aliasPrefix?: string | null
|
||||||
|
tags: SnPostTag[]
|
||||||
|
categories: SnPostCategory[]
|
||||||
|
replies?: SnPost[] | null
|
||||||
|
replyId?: number | null
|
||||||
|
repostId?: number | null
|
||||||
|
replyTo?: SnPost | null
|
||||||
|
repostTo?: SnPost | null
|
||||||
|
visibleUsersList?: number[] | null
|
||||||
|
invisibleUsersList?: number[] | null
|
||||||
|
visibility: number
|
||||||
|
editedAt?: Date | null
|
||||||
|
pinnedAt?: Date | null
|
||||||
|
lockedAt?: Date | null
|
||||||
|
isDraft: boolean
|
||||||
|
publishedAt?: Date | null
|
||||||
|
publishedUntil?: Date | null
|
||||||
|
totalUpvote: number
|
||||||
|
totalDownvote: number
|
||||||
|
publisherId: number
|
||||||
|
publisher: SnPublisher
|
||||||
|
metric: SnMetric
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SnPostTag {
|
||||||
|
id: number
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
deletedAt?: Date
|
||||||
|
alias: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
posts?: SnPost[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SnPostCategory {
|
||||||
|
id: number
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
deletedAt?: Date
|
||||||
|
alias: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
posts?: SnPost[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SnPostBody {
|
||||||
|
attachments: string[]
|
||||||
|
content: string
|
||||||
|
location?: string
|
||||||
|
thumbnail?: string
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SnMetric {
|
||||||
|
replyCount: number
|
||||||
|
reactionCount: number
|
||||||
|
reactionList: Record<string, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SnPublisher {
|
||||||
|
id: number
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
deletedAt?: Date | null
|
||||||
|
type: number
|
||||||
|
name: string
|
||||||
|
nick: string
|
||||||
|
description: string
|
||||||
|
avatar: string
|
||||||
|
banner: string
|
||||||
|
totalUpvote: number
|
||||||
|
totalDownvote: number
|
||||||
|
realmId?: number | null
|
||||||
|
accountId: number
|
||||||
|
}
|
@ -1,18 +1,18 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
background: "var(--background)",
|
background: 'var(--background)',
|
||||||
foreground: "var(--foreground)",
|
foreground: 'var(--foreground)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [require('@tailwindcss/typography')],
|
||||||
} satisfies Config;
|
} satisfies Config
|
||||||
|
Loading…
Reference in New Issue
Block a user