Basic post preview

This commit is contained in:
LittleSheep 2025-01-04 00:37:26 +08:00
parent a56f098413
commit 02ccbdac71
7 changed files with 254 additions and 9 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -17,6 +17,7 @@
"@mui/material": "^6.3.0",
"@mui/material-nextjs": "^6.3.1",
"@mui/x-charts": "^7.23.2",
"@tailwindcss/typography": "^0.5.15",
"axios": "^1.7.9",
"axios-case-converter": "^1.1.1",
"cookies-next": "^5.0.2",
@ -24,6 +25,11 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"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"
},
"devDependencies": {

View File

@ -41,7 +41,7 @@ function ElevationScroll(props: ElevationAppBarProps) {
? React.cloneElement(props.children, {
elevation: trigger ? props.elevation : 0,
sx: trigger
? { backgroundColor: props.color, ...commonStyle }
? { backgroundColor: props.color, color: 'black', ...commonStyle }
: { backgroundColor: 'transparent', ...commonStyle },
})
: null

106
src/pages/posts/[...id].tsx Normal file
View 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>
</>
)
}

View 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
View 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
}

View File

@ -1,18 +1,18 @@
import type { Config } from "tailwindcss";
import type { Config } from 'tailwindcss'
export default {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
background: 'var(--background)',
foreground: 'var(--foreground)',
},
},
},
plugins: [],
} satisfies Config;
plugins: [require('@tailwindcss/typography')],
} satisfies Config